]> gitweb.fperrin.net Git - DictionaryPC.git/blob - src/com/hughes/android/dictionary/parser/WikiTokenizer.java
Optimize escapedFindEnd.
[DictionaryPC.git] / src / com / hughes / android / dictionary / parser / WikiTokenizer.java
1 // Copyright 2011 Google Inc. All Rights Reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package com.hughes.android.dictionary.parser;
16
17 import java.util.*;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20
21 public final class WikiTokenizer {
22
23     public interface Callback {
24         void onPlainText(final String text);
25         void onMarkup(WikiTokenizer wikiTokenizer);
26         void onWikiLink(WikiTokenizer wikiTokenizer);
27         void onNewline(WikiTokenizer wikiTokenizer);
28         void onFunction(final WikiTokenizer tokenizer, String functionName, List<String> functionPositionArgs,
29                         Map<String, String> functionNamedArgs);
30         void onHeading(WikiTokenizer wikiTokenizer);
31         void onListItem(WikiTokenizer wikiTokenizer);
32         void onComment(WikiTokenizer wikiTokenizer);
33         void onHtml(WikiTokenizer wikiTokenizer);
34     }
35
36     public static class DoNothingCallback implements Callback {
37
38         @Override
39         public void onPlainText(String text) {
40         }
41
42         @Override
43         public void onMarkup(WikiTokenizer wikiTokenizer) {
44         }
45
46         @Override
47         public void onWikiLink(WikiTokenizer wikiTokenizer) {
48         }
49
50         @Override
51         public void onNewline(WikiTokenizer wikiTokenizer) {
52         }
53
54         @Override
55         public void onFunction(WikiTokenizer tokenizer, String functionName,
56                                List<String> functionPositionArgs, Map<String, String> functionNamedArgs) {
57         }
58
59         @Override
60         public void onHeading(WikiTokenizer wikiTokenizer) {
61         }
62
63         @Override
64         public void onListItem(WikiTokenizer wikiTokenizer) {
65         }
66
67         @Override
68         public void onComment(WikiTokenizer wikiTokenizer) {
69         }
70
71         @Override
72         public void onHtml(WikiTokenizer wikiTokenizer) {
73         }
74     }
75
76     //private static final Pattern wikiTokenEvent = Pattern.compile("($)", Pattern.MULTILINE);
77     private static final Pattern wikiTokenEvent = Pattern.compile(
78             "\\{\\{|\\}\\}|" +
79             "\\[\\[|\\]\\]|" +
80             "\\||" +  // Need the | because we might have to find unescaped pipes
81             "=|" +  // Need the = because we might have to find unescaped =
82             "<!--|" +
83             "''|" +
84             "<pre>|" +
85             "<math>|" +
86             "<ref>|" +
87             "\n", Pattern.MULTILINE);
88     private static final String listChars = "*#:;";
89
90
91     final String wikiText;
92     final Matcher matcher;
93
94     boolean justReturnedNewline = true;
95     int lastLineStart = 0;
96     int end = 0;
97     int start = -1;
98
99     final List<String> errors = new ArrayList<>();
100     final List<TokenDelim> tokenStack = new ArrayList<>();
101
102
103     private String headingWikiText;
104     private int headingDepth;
105     private int listPrefixEnd;
106     private boolean isPlainText;
107     private boolean isMarkup;
108     private boolean isComment;
109     private boolean isFunction;
110     private boolean isWikiLink;
111     private boolean isHtml;
112     private int firstUnescapedPipePos;
113
114     private int lastUnescapedPipePos;
115     private int lastUnescapedEqualsPos;
116     private final List<String> positionArgs = new ArrayList<>();
117     private final Map<String,String> namedArgs = new LinkedHashMap<>();
118
119
120     public WikiTokenizer(final String wikiText) {
121         this(wikiText, true);
122     }
123
124     public WikiTokenizer(String wikiText, final boolean isNewline) {
125         wikiText = wikiText.replace('\u2028', '\n');
126         wikiText = wikiText.replace('\u2029', '\n');
127         wikiText = wikiText.replace('\u0085', '\n');
128         this.wikiText = wikiText;
129         this.matcher = wikiTokenEvent.matcher(wikiText);
130         justReturnedNewline = isNewline;
131     }
132
133     private void clear() {
134         errors.clear();
135         tokenStack.clear();
136
137         headingWikiText = null;
138         headingDepth = -1;
139         listPrefixEnd = -1;
140         isPlainText = false;
141         isMarkup = false;
142         isComment = false;
143         isFunction = false;
144         isWikiLink = false;
145         isHtml = false;
146
147         firstUnescapedPipePos = -1;
148         lastUnescapedPipePos = -1;
149         lastUnescapedEqualsPos = -1;
150         positionArgs.clear();
151         namedArgs.clear();
152     }
153
154     private static final Matcher POSSIBLE_WIKI_TEXT = Pattern.compile(
155                 "\\{\\{|" +
156                 "\\[\\[|" +
157                 "<!--|" +
158                 "''|" +
159                 "<pre>|" +
160                 "<math>|" +
161                 "<ref>|" +
162                 "\n"
163             ).matcher("");
164
165     public static void dispatch(final String wikiText, final boolean isNewline, final Callback callback) {
166         // Statistical background, from EN-DE dictionary generation:
167         // out of 12083000 calls, 9697686 can be skipped via the test
168         // for ', \n and ((c - 0x3b) & 0xff9f) < 2 (which covers among others
169         // <, { and [).
170         // This increased to 10006466 checking for <, { and [ specifically,
171         // and is minimally faster overall.
172         // A even more precise one using regex and checking for {{, [[, <!--, '',
173         // <pre>, <math>, <ref> and \n increased that to 10032846.
174         // Regex thus seems far too costly for a measly increase from 80%/82% to 83% rejection rate
175         // However completely removing it changes output (likely a bug), so leave it in for now
176         // but at least run it only on the 18% not caught by the faster logic.
177         // Original runtime: 1m29.708s
178         // Optimized: 1m19.170s
179         // Regex removed: 1m20.314s (not statistically significant)
180         boolean matched = false;
181         for (int i = 0; i < wikiText.length(); i++) {
182             int c = wikiText.charAt(i);
183             if (c == '\'' || c == '\n' || c == '<' || c == '[' || c == '{') {
184                 matched = true;
185                 break;
186             }
187         }
188         if (!matched || !POSSIBLE_WIKI_TEXT.reset(wikiText).find()) {
189             callback.onPlainText(wikiText);
190         } else {
191             final WikiTokenizer tokenizer = new WikiTokenizer(wikiText, isNewline);
192             while (tokenizer.nextToken() != null) {
193                 if (tokenizer.isPlainText()) {
194                     callback.onPlainText(tokenizer.token());
195                 } else if (tokenizer.isMarkup()) {
196                     callback.onMarkup(tokenizer);
197                 } else if (tokenizer.isWikiLink()) {
198                     callback.onWikiLink(tokenizer);
199                 } else if (tokenizer.isNewline()) {
200                     callback.onNewline(tokenizer);
201                 } else if (tokenizer.isFunction()) {
202                     callback.onFunction(tokenizer, tokenizer.functionName(), tokenizer.functionPositionArgs(), tokenizer.functionNamedArgs());
203                 } else if (tokenizer.isHeading()) {
204                     callback.onHeading(tokenizer);
205                 } else if (tokenizer.isListItem()) {
206                     callback.onListItem(tokenizer);
207                 } else if (tokenizer.isComment()) {
208                     callback.onComment(tokenizer);
209                 } else if (tokenizer.isHtml()) {
210                     callback.onHtml(tokenizer);
211                 } else if (!tokenizer.errors.isEmpty()) {
212                     // Log was already printed....
213                 } else {
214                     throw new IllegalStateException("Unknown wiki state: " + tokenizer.token());
215                 }
216             }
217         }
218     }
219
220     public List<String> errors() {
221         return errors;
222     }
223
224     public boolean isNewline() {
225         return justReturnedNewline;
226     }
227
228     public void returnToLineStart() {
229         end = start = lastLineStart;
230         justReturnedNewline = true;
231     }
232
233     public boolean isHeading() {
234         return headingWikiText != null;
235     }
236
237     public String headingWikiText() {
238         assert isHeading();
239         return headingWikiText;
240     }
241
242     public int headingDepth() {
243         assert isHeading();
244         return headingDepth;
245     }
246
247     public boolean isMarkup() {
248         return isMarkup;
249     }
250
251     public boolean isComment() {
252         return isComment;
253     }
254
255     public boolean isListItem() {
256         return listPrefixEnd != -1;
257     }
258
259     public String listItemPrefix() {
260         assert isListItem();
261         return wikiText.substring(start, listPrefixEnd);
262     }
263
264     public static String getListTag(char c) {
265         if (c == '#') {
266             return "ol";
267         }
268         return "ul";
269     }
270
271     public String listItemWikiText() {
272         assert isListItem();
273         return wikiText.substring(listPrefixEnd, end);
274     }
275
276     public boolean isFunction() {
277         return isFunction;
278     }
279
280     public String functionName() {
281         assert isFunction();
282         // "{{.."
283         if (firstUnescapedPipePos != -1) {
284             return trimNewlines(wikiText.substring(start + 2, firstUnescapedPipePos).trim());
285         }
286         final int safeEnd = Math.max(start + 2, end - 2);
287         return trimNewlines(wikiText.substring(start + 2, safeEnd).trim());
288     }
289
290     public List<String> functionPositionArgs() {
291         return positionArgs;
292     }
293
294     public Map<String, String> functionNamedArgs() {
295         return namedArgs;
296     }
297
298     public boolean isPlainText() {
299         return isPlainText;
300     }
301
302     public boolean isWikiLink() {
303         return isWikiLink;
304     }
305
306     public String wikiLinkText() {
307         assert isWikiLink();
308         // "[[.."
309         if (lastUnescapedPipePos != -1) {
310             return trimNewlines(wikiText.substring(lastUnescapedPipePos + 1, end - 2));
311         }
312         assert start + 2 < wikiText.length() && end >= 2: wikiText;
313         return trimNewlines(wikiText.substring(start + 2, end - 2));
314     }
315
316     public String wikiLinkDest() {
317         assert isWikiLink();
318         // "[[.."
319         if (firstUnescapedPipePos != -1) {
320             return trimNewlines(wikiText.substring(start + 2, firstUnescapedPipePos));
321         }
322         return null;
323     }
324
325     public boolean isHtml() {
326         return isHtml;
327     }
328
329     public boolean remainderStartsWith(final String prefix) {
330         return wikiText.startsWith(prefix, start);
331     }
332
333     public void nextLine() {
334         final int oldStart = start;
335         while(nextToken() != null && !isNewline()) {}
336         if (isNewline()) {
337             --end;
338         }
339         start = oldStart;
340     }
341
342
343     public WikiTokenizer nextToken() {
344         this.clear();
345
346         start = end;
347
348         if (justReturnedNewline) {
349             lastLineStart = start;
350         }
351
352         try {
353
354             final int len = wikiText.length();
355             if (start >= len) {
356                 return null;
357             }
358
359             // Eat a newline if we're looking at one:
360             final boolean atNewline = wikiText.charAt(end) == '\n';
361             if (atNewline) {
362                 justReturnedNewline = true;
363                 ++end;
364                 return this;
365             }
366
367             if (justReturnedNewline) {
368                 justReturnedNewline = false;
369
370                 final char firstChar = wikiText.charAt(end);
371                 if (firstChar == '=') {
372                     final int headerStart = end;
373                     // Skip ===...
374                     while (++end < len && wikiText.charAt(end) == '=') {}
375                     final int headerTitleStart = end;
376                     headingDepth = headerTitleStart - headerStart;
377                     // Skip non-=...
378                     if (end < len) {
379                         final int nextNewline = safeIndexOf(wikiText, end, "\n", "\n");
380                         final int closingEquals = escapedFindEnd(end, TokenDelim.EQUALS);
381                         if (wikiText.charAt(closingEquals - 1) == '=') {
382                             end = closingEquals - 1;
383                         } else {
384                             end = nextNewline;
385                         }
386                     }
387                     final int headerTitleEnd = end;
388                     headingWikiText = wikiText.substring(headerTitleStart, headerTitleEnd);
389                     // Skip ===...
390                     while (end < len && ++end < len && wikiText.charAt(end) == '=') {}
391                     final int headerEnd = end;
392                     if (headerEnd - headerTitleEnd != headingDepth) {
393                         errors.add("Mismatched header depth: " + token());
394                     }
395                     return this;
396                 }
397                 if (listChars.indexOf(firstChar) != -1) {
398                     while (++end < len && listChars.indexOf(wikiText.charAt(end)) != -1) {}
399                     listPrefixEnd = end;
400                     end = escapedFindEnd(start, TokenDelim.NEWLINE);
401                     return this;
402                 }
403             }
404
405             if (wikiText.startsWith("'''", start)) {
406                 isMarkup = true;
407                 end = start + 3;
408                 return this;
409             }
410
411             if (wikiText.startsWith("''", start)) {
412                 isMarkup = true;
413                 end = start + 2;
414                 return this;
415             }
416
417             if (wikiText.startsWith("[[", start)) {
418                 end = escapedFindEnd(start + 2, TokenDelim.DBRACKET_CLOSE);
419                 isWikiLink = errors.isEmpty();
420                 return this;
421             }
422
423             if (wikiText.startsWith("{{", start)) {
424                 end = escapedFindEnd(start + 2, TokenDelim.BRACE_CLOSE);
425                 isFunction = errors.isEmpty();
426                 return this;
427             }
428
429             if (wikiText.startsWith("<pre>", start)) {
430                 end = safeIndexOf(wikiText, start, "</pre>", "\n");
431                 isHtml = true;
432                 return this;
433             }
434
435             if (wikiText.startsWith("<ref>", start)) {
436                 end = safeIndexOf(wikiText, start, "</ref>", "\n");
437                 isHtml = true;
438                 return this;
439             }
440
441             if (wikiText.startsWith("<math>", start)) {
442                 end = safeIndexOf(wikiText, start, "</math>", "\n");
443                 isHtml = true;
444                 return this;
445             }
446
447             if (wikiText.startsWith("<!--", start)) {
448                 isComment = true;
449                 end = safeIndexOf(wikiText, start, "-->", "\n");
450                 return this;
451             }
452
453             if (wikiText.startsWith("}}", start) || wikiText.startsWith("]]", start)) {
454                 errors.add("Close without open!");
455                 end += 2;
456                 return this;
457             }
458
459             if (wikiText.charAt(start) == '|' || wikiText.charAt(start) == '=') {
460                 isPlainText = true;
461                 ++end;
462                 return this;
463             }
464
465
466             while (end < wikiText.length()) {
467                 int c = wikiText.charAt(end);
468                 if (c == '\n' || c == '\'' || ((c - 0x1b) & 0xff9f) < 3) {
469                     matcher.region(end, wikiText.length());
470                     if (matcher.lookingAt()) break;
471                 }
472                 end++;
473             }
474             if (end != wikiText.length()) {
475                 isPlainText = true;
476                 if (end == start) {
477                     // stumbled over a new type of newline?
478                     // Or matcher is out of sync with checks above
479                     errors.add("Empty group: " + this.matcher.group() + " char: " + (int)wikiText.charAt(end));
480                     assert false;
481                     // Note: all newlines should be normalize to \n before calling this function
482                     throw new RuntimeException("matcher not in sync with code, or new type of newline, errors :" + errors);
483                 }
484                 return this;
485             }
486
487             isPlainText = true;
488             return this;
489
490         } finally {
491             if (!errors.isEmpty()) {
492                 System.err.println("Errors: " + errors + ", token=" + token());
493             }
494         }
495
496     }
497
498     public String token() {
499         final String token = wikiText.substring(start, end);
500         assert token.equals("\n") || !token.endsWith("\n") : "token='" + token + "'";
501         return token;
502     }
503
504     enum TokenDelim { NEWLINE, BRACE_OPEN, BRACE_CLOSE, DBRACKET_OPEN, DBRACKET_CLOSE, BRACKET_OPEN, BRACKET_CLOSE, PIPE, EQUALS, COMMENT }
505
506     private int tokenDelimLen(TokenDelim d) {
507         switch (d) {
508             case NEWLINE:
509             case BRACKET_OPEN:
510             case BRACKET_CLOSE:
511             case PIPE:
512             case EQUALS:
513                 return 1;
514             case BRACE_OPEN:
515             case BRACE_CLOSE:
516             case DBRACKET_OPEN:
517             case DBRACKET_CLOSE:
518                 return 2;
519             case COMMENT:
520                 return 4;
521             default:
522                 throw new RuntimeException();
523         }
524     }
525
526     static final String[] patterns = { "\n", "{{", "}}", "[[", "]]", "[", "]", "|", "=", "<!--" };
527     private int escapedFindEnd(final int start, final TokenDelim toFind) {
528         assert tokenStack.isEmpty();
529
530         final boolean insideFunction = toFind == TokenDelim.BRACE_CLOSE;
531
532         int end = start;
533         int firstNewline = -1;
534         int singleBrackets = 0;
535         while (end < wikiText.length()) {
536             // Manual replacement for matcher.find(end),
537             // because Java regexp is a ridiculously slow implementation.
538             // Initialize to always match the end.
539             TokenDelim match = TokenDelim.NEWLINE;
540             int matchStart = end;
541             for (; matchStart < wikiText.length(); matchStart++) {
542                 int i = matchStart;
543                 int c = wikiText.charAt(i);
544                 if (c == '\n') break;
545                 if (c == '{' && wikiText.startsWith("{{", i)) { match = TokenDelim.BRACE_OPEN; break; }
546                 if (c == '}' && wikiText.startsWith("}}", i)) { match = TokenDelim.BRACE_CLOSE; break; }
547                 if (c == '[') { match = wikiText.startsWith("[[", i) ? TokenDelim.DBRACKET_OPEN : TokenDelim.BRACKET_OPEN ; break; }
548                 if (c == ']') { match = wikiText.startsWith("]]", i) ? TokenDelim.DBRACKET_CLOSE : TokenDelim.BRACKET_CLOSE ; break; }
549                 if (c == '|') { match = TokenDelim.PIPE; break; }
550                 if (c == '=') { match = TokenDelim.EQUALS; break; }
551                 if (c == '<' && wikiText.startsWith("<!--", i)) { match = TokenDelim.COMMENT; break; }
552             }
553
554             int matchEnd = matchStart + (match == TokenDelim.NEWLINE ? 0 : tokenDelimLen(match));
555             if (match != TokenDelim.NEWLINE && tokenStack.isEmpty() && match == toFind) {
556                 // The normal return....
557                 if (insideFunction) {
558                     addFunctionArg(insideFunction, matchStart);
559                 }
560                 return matchEnd;
561             }
562             switch (match) {
563                 case NEWLINE:
564                 assert matchStart == wikiText.length() || wikiText.charAt(matchStart) == '\n' : wikiText + ", " + matchStart;
565                 if (firstNewline == -1) {
566                     firstNewline = matchEnd;
567                 }
568                 if (tokenStack.isEmpty() && toFind == TokenDelim.NEWLINE) {
569                     return matchStart;
570                 }
571                 ++end;
572                 break;
573                 case BRACKET_OPEN:
574                 singleBrackets++;
575                 break;
576                 case BRACKET_CLOSE:
577                 if (singleBrackets > 0) singleBrackets--;
578                 break;
579                 case DBRACKET_OPEN:
580                 case BRACE_OPEN:
581                 tokenStack.add(match);
582                 break;
583                 case DBRACKET_CLOSE:
584                 case BRACE_CLOSE:
585                 if (!tokenStack.isEmpty()) {
586                     final TokenDelim removed = tokenStack.remove(tokenStack.size() - 1);
587                     if (removed == TokenDelim.BRACE_OPEN && match != TokenDelim.BRACE_CLOSE) {
588                         if (singleBrackets >= 2) { // assume this is really two closing single ]
589                             singleBrackets -= 2;
590                             tokenStack.add(removed);
591                         } else {
592                             errors.add("Unmatched {{ error: " + wikiText.substring(start, matchEnd));
593                             return safeIndexOf(wikiText, start, "\n", "\n");
594                         }
595                     } else if (removed == TokenDelim.DBRACKET_OPEN && match != TokenDelim.DBRACKET_CLOSE) {
596                         errors.add("Unmatched [[ error: " + wikiText.substring(start, matchEnd));
597                         return safeIndexOf(wikiText, start, "\n", "\n");
598                     }
599                 } else {
600                     errors.add("Pop too many " + wikiText.substring(matchStart, matchEnd) + " error: " + wikiText.substring(start, matchEnd).replace("\n", "\\\\n"));
601                     // If we were looking for a newline
602                     return safeIndexOf(wikiText, start, "\n", "\n");
603                 }
604                 break;
605                 case PIPE:
606                 if (tokenStack.isEmpty()) {
607                     addFunctionArg(insideFunction, matchStart);
608                 }
609                 break;
610                 case EQUALS:
611                 if (tokenStack.isEmpty()) {
612                     lastUnescapedEqualsPos = matchStart;
613                 }
614                 // Do nothing.  These can match spuriously, and if it's not the thing
615                 // we're looking for, keep on going.
616                 break;
617                 case COMMENT:
618                 end = wikiText.indexOf("-->", matchStart);
619                 if (end == -1) {
620                     errors.add("Unmatched <!-- error: " + wikiText.substring(start));
621                     return safeIndexOf(wikiText, start, "\n", "\n");
622                 }
623                 break;
624                 default:
625                     throw new RuntimeException();
626             }
627
628             // Inside the while loop.  Just go forward.
629             end = Math.max(end, matchEnd);
630         }
631         if (toFind == TokenDelim.NEWLINE && tokenStack.isEmpty()) {
632             // We were looking for the end, we got it.
633             return end;
634         }
635         errors.add("Couldn't find: " + toFind + ", "+ wikiText.substring(start));
636         if (firstNewline != -1) {
637             return firstNewline;
638         }
639         return end;
640     }
641
642     private void addFunctionArg(final boolean insideFunction, final int matchStart) {
643         if (firstUnescapedPipePos == -1) {
644             firstUnescapedPipePos = lastUnescapedPipePos = matchStart;
645         } else if (insideFunction) {
646             if (lastUnescapedEqualsPos > lastUnescapedPipePos) {
647                 final String key = wikiText.substring(lastUnescapedPipePos + 1, lastUnescapedEqualsPos);
648                 final String value = wikiText.substring(lastUnescapedEqualsPos + 1, matchStart);
649                 namedArgs.put(trimNewlines(key), trimNewlines(value));
650             } else {
651                 final String value = wikiText.substring(lastUnescapedPipePos + 1, matchStart);
652                 positionArgs.add(trimNewlines(value));
653             }
654         }
655         lastUnescapedPipePos = matchStart;
656     }
657
658     static String trimNewlines(String s) {
659         while (s.startsWith("\n")) {
660             s = s.substring(1);
661         }
662         while (s.endsWith("\n")) {
663             s = s.substring(0, s.length() - 1);
664         }
665         return s.replace('\n', ' ');
666     }
667
668     static int safeIndexOf(final String s, final int start, final String target, final String backup) {
669         int close = s.indexOf(target, start);
670         if (close != -1) {
671             // Don't step over a \n.
672             return close + (target.equals("\n") ? 0 : target.length());
673         }
674         close = s.indexOf(backup, start);
675         if (close != -1) {
676             return close + (backup.equals("\n") ? 0 : backup.length());
677         }
678         return s.length();
679     }
680
681     public static String toPlainText(final String wikiText) {
682         final WikiTokenizer wikiTokenizer = new WikiTokenizer(wikiText);
683         final StringBuilder builder = new StringBuilder();
684         while (wikiTokenizer.nextToken() != null) {
685             if (wikiTokenizer.isPlainText()) {
686                 builder.append(wikiTokenizer.token());
687             } else if (wikiTokenizer.isWikiLink()) {
688                 builder.append(wikiTokenizer.wikiLinkText());
689             } else if (wikiTokenizer.isNewline()) {
690                 builder.append("\n");
691             } else if (wikiTokenizer.isFunction()) {
692                 builder.append(wikiTokenizer.token());
693             }
694         }
695         return builder.toString();
696     }
697
698     public static StringBuilder appendFunction(final StringBuilder builder, final String name, List<String> args,
699             final Map<String, String> namedArgs) {
700         builder.append(name);
701         for (final String arg : args) {
702             builder.append("|").append(arg);
703         }
704         for (final Map.Entry<String, String> entry : namedArgs.entrySet()) {
705             builder.append("|").append(entry.getKey()).append("=").append(entry.getValue());
706         }
707         return builder;
708     }
709
710 }