1
votes

Please pardon my lack of proper terminology, as I'm sure there's a term for this. I'm writing XML text using raw strings (not with any type of XML builder/parser, for ease of use). However, I'm facing an issue where some characters in the data I'm providing throw off the standardization. For example, the & symbol. When a string includes this, the end parser gets thrown off. How do I accommodate for this properly and convert strings to XML standards?

I'm writing plain strings to a string list and reading its Text property like below. Note the subroutine A(const S: String); which is a shortened method of adding a line to the XML file and adds a necessary indent. See the subroutine Standardize, this is what I need to fill in.

uses Windows, Classes, SysUtils, DB, ADODB, ActiveX;

function TSomething.FetchXML(const SQL: String): String;
var
  L: TStringList;
  Q: TADOQuery;
  X, Y: Integer;
  function Standardize(const S: String): String;
  begin
    Result:= S; //<<<--- Need to convert string to XML standards
  end;
  procedure A(const Text: String; const Indent: Integer = 0);
  var
    I: Integer;
    S: String;
  begin
    if Indent > 0 then
      for I := 0 to Indent do
        S:= S + '  ';
    L.Append(S + Text);
  end;
begin
  Result:= '';
  L:= TStringList.Create;
  try
    Q:= TADOQuery.Create(nil);
    try
      Q.ConnectionString:= FCredentials.ConnectionString;
      Q.SQL.Text:= SQL;
      Q.Open;
      A('<?xml version="1.0" encoding="UTF-8"?>');
      A('<dataset Source="ECatAPI">');
      A('<table>');
      A('<fields>', 1);
      for X := 0 to Q.FieldCount - 1 do begin
        A('<field Name="'+Q.Fields[X].FieldName+'" '+
          'Type="'+IntToStr(Integer(Q.Fields[X].DataType))+'" '+
          'Width="'+IntToStr(Q.Fields[X].DisplayWidth)+'" />', 2);
      end;
      A('</fields>', 1);
      A('<rows>', 1);
      if not Q.IsEmpty then begin
        Q.First;
        while not Q.Eof do begin
          A('<row>', 2);
          for Y:= 0 to Q.FieldCount - 1 do begin
            A('<value Field="'+Q.Fields[Y].FieldName+'">'+
              Standardize(Q.Fields[Y].AsString)+'</value>', 3);
          end;
          A('</row>', 2);
          Q.Next;
        end;
      end;
      A('</rows>', 1);
      A('</table>');
      A('</dataset>');
      Result:= L.Text;
      Q.Close;
    finally
      Q.Free;
    end;
  finally
    L.Free;
  end;
end;

NOTE

The above is pseudo-code, copied and modified, irrelevant things have been altered/excluded...

MORE INFO

This application is a stand-alone web server providing read-only access to data. I only need to write XML data, I don't need to read it. And even if I do, I have an XML parser library covering that part already. I'm trying to keep this light-weight as possible, without filling the memory with unnecessary objects.

5
The terminology you're looking for is called "escaping" or "encoding", and writing your own XML writing or parsing code is a serious waste of time. There are dozens of lightweight XML libraries out there that are free (OmniXML, for instance). This sounds like a severe case of NIH (not invented here) syndrome. :-) - Ken White
Here's the list : en.wikipedia.org/wiki/… - but you should really make things easier on yourself and find an XML library.. - Blorgbeard
@Deltics: Except that's not what happens. :-) Next, there will be "well, I have this code that writes XML. I can just modify it to support ...". NIH is pretty well known for this exact reason - reinventing the wheel. When I go buy a new car, I don't tell the dealer "Leave the wheels and tires off; I have a hammer, chisel, and a bunch of big rocks at home". ;-) - Ken White
@Jerry: It doesn't matter whether you're reading or writing. Using a proper XML library makes sure that things are properly encoded, tags are properly closed, and can even validate against a DTD or XSL schema to make sure data types are correct and values fit ranges or requirements (not null, etc). See my last comment to Deltics. - Ken White
Personally I'd use an XML library because that makes the code more readable and maintainable. I notice that you used a string list and lots of StringReplace calls. If you so concerned about memory why didn't you optimise those out? And why are you using TADOQuery? Loads of memory used there. A good lightweight XML emitter would use less memory than this code! This is pointless micro (non)-optimisation. - David Heffernan

5 Answers

4
votes

Do not generate XML by hand PERIOD.

Writing correct code for escaping complex data (for instance XML, HTML or other SGML in XML, escaped CDATA) is not worth it.

The escaping you do is just a start. Wait until someone puts something in your data that is incompatible.

Many databases support creating well formed XML from queries anyway (see the other answers), that is a direction you should be looking into.

3
votes

Another tip: Maybe your database supports generating results as XML.

1
votes

Thanks to the comments above in the question, I've implemented a function to replace predefined entities with the appropriate name. This is the new subroutine:

function EncodeXmlStr(const S: String): String;
begin
  Result:= StringReplace(S,      '&',  '&amp;',  [rfReplaceAll]);
  Result:= StringReplace(Result, '''', '&apos;', [rfReplaceAll]);
  Result:= StringReplace(Result, '"',  '&quot;', [rfReplaceAll]);
  Result:= StringReplace(Result, '<',  '&lt;',   [rfReplaceAll]);
  Result:= StringReplace(Result, '>',  '&gt;',   [rfReplaceAll]);
end;
1
votes

Jerry' solution is a good one.

It's worth noting that there are existing VCL procedures to do this.

unit IdStrings has StrXHtmlEncode(). This is identical to Jerry's solution.

unit HttpApp has HTMLEncode(). This function is more efficient that Jerry's solution - but be warned - this procedure is actually broken for unicode strings. It worked correctly in pre unicode compilers, but was not correctly upgraded for unicode, and the error has never been fixed.

A unicode safe version of HttpApp.HTMLEncode(), with the apos replacement added, is as follows. It's more verbose that the StringReplace() style, but a lot more efficient in terms of run-time performance. (apos is a predefined entity for XML and XHTHML, but not for HTML 4).

function XHTMLEncode( const sRawValue: string): string;
var
  Sp, Rp: PChar;
begin
  SetLength( result, Length( sRawValue) * 10);
  Sp := PChar( sRawValue);
  Rp := PChar( result);
  while Sp^ <> #0 do
  begin
    case Sp^ of
      '&': begin
             FormatBuf( Rp^, 10, '&amp;', 10, []);
             Inc(Rp,4);
           end;
      '<',
      '>': begin
             if Sp^ = '<' then
               FormatBuf(Rp^, 8, '&lt;', 8, [])
             else
               FormatBuf(Rp^, 8, '&gt;', 8, []);
             Inc(Rp,3);
           end;
      '"': begin
             FormatBuf(Rp^, 12, '&quot;', 12, []);
             Inc(Rp,5);
           end;
      '''': begin
             FormatBuf(Rp^, 12, '&apos;', 12, []);
             Inc(Rp,5);
           end;
    else
      Rp^ := Sp^
    end;
    Inc(Rp);
    Inc(Sp);
  end;
  SetLength( result, Rp - PChar( result))
end;
0
votes

The answer of Sean B. Durkin is now deprecated due to moving of FormatBuf to ansistring. Also his implementation will not work on all platforms. So I have written a better solution using a stringbuilder.

function HTMLEncodeStr(const aStr: String): string;
var
  c: Char;
  sb: TStringbuilder;
begin
  sb := TStringbuilder.Create;
  try
    for c in aStr do
    begin
      if      (c = '<')  then sb.Append('&lt;')
      else if (c = '>')  then sb.Append('&gt;')
      else if (c = '&')  then sb.Append('&amp;')
      else if (c = '"')  then sb.Append('&quot;')
      else if (c = '''') then sb.Append('&apos;')
      else sb.Append(c);
    end;
    result := sb.ToString;
  finally
    sb.Free;
  end;
end;