/* * Copyright 2001-2008 Geert Bevin (gbevin[remove] at uwyn dot com) * Licensed under the Apache License, Version 2.0 (the "License") * $Id: Parser.java 3918 2008-04-14 17:35:35Z gbevin $ */ package com.uwyn.rife.template; import com.uwyn.rife.template.exceptions.*; import java.util.*; import com.uwyn.rife.config.RifeConfig; import com.uwyn.rife.datastructures.DocumentPosition; import com.uwyn.rife.datastructures.collections.primitives.ArrayIntList; import com.uwyn.rife.resources.ResourceFinder; import com.uwyn.rife.resources.exceptions.ResourceFinderErrorException; import com.uwyn.rife.tools.StringUtils; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Parser implements Cloneable { public enum PartType {CONTENT, TAG_START, TAG_END, TAG_TERM, TAG_SHORT_TERM, STRING_DELIMITER_BEGIN, STRING_DELIMITER_END, VALUE, BLOCK, BLOCKVALUE, BLOCKAPPEND, COMMENT, INCLUDE, UNESCAPE_START} public final static String DEFAULT_TEMPLATES_PATH = "templates/"; public final static String TEMPLATE_PACKAGE = "com.uwyn.rife.template."; private TemplateFactory mTemplateFactory = null; private String mIdentifier = null; private String mPackageName = null; private Config[] mConfigs = null; private String mAmbiguousName = null; private String mExtension = null; private int mExtensionLength = -1; private Pattern[] mBlockFilters = null; private Pattern[] mValueFilters = null; Parser(TemplateFactory templateFactory, String identifier, Config[] configs, String extension, Pattern[] blockFilters, Pattern[] valueFilters) { init(templateFactory, identifier, configs, extension, blockFilters, valueFilters); } Config[] getConfigs() { return mConfigs; } String getExtension() { return mExtension; } Pattern[] getBlockFilters() { return mBlockFilters; } Pattern[] getValueFilters() { return mValueFilters; } TemplateFactory getTemplateFactory() { return mTemplateFactory; } private void init(TemplateFactory templateFactory, String identifier, Config[] configs, String extension, Pattern[] blockFilters, Pattern[] valueFilters) { assert templateFactory != null; assert identifier != null; assert configs != null; assert configs.length > 0; assert extension != null; mTemplateFactory = templateFactory; mIdentifier = identifier; mPackageName = TEMPLATE_PACKAGE + mIdentifier + "."; mConfigs = configs; mExtension = extension; mExtensionLength = mExtension.length(); mAmbiguousName = (mExtension + mExtension).substring(1); mBlockFilters = blockFilters; mValueFilters = valueFilters; assert mExtensionLength > 0; } public Parser clone() { Parser new_parser = null; try { new_parser = (Parser)super.clone(); } catch (CloneNotSupportedException e) { new_parser = null; } Pattern[] new_blockfilters = null; if (mBlockFilters != null) { new_blockfilters = mBlockFilters.clone(); } Pattern[] new_valuefilters = null; if (mValueFilters != null) { new_valuefilters = mValueFilters.clone(); } new_parser.init(mTemplateFactory, mIdentifier, mConfigs, mExtension, new_blockfilters, new_valuefilters); return new_parser; } public boolean equals(Object object) { if (object == this) { return true; } if (null == object) { return false; } if (!(object instanceof Parser)) { return false; } Parser other_parser = (Parser)object; if (!Arrays.equals(other_parser.mConfigs, this.mConfigs)) { return false; } if (!other_parser.mIdentifier.equals(this.mIdentifier)) { return false; } if (!other_parser.mExtension.equals(this.mExtension)) { return false; } if (null == other_parser.mBlockFilters && this.mBlockFilters != null || other_parser.mBlockFilters != null && null == this.mBlockFilters) { return false; } if (other_parser.mBlockFilters != null && this.mBlockFilters != null) { if (other_parser.mBlockFilters.length != this.mBlockFilters.length) { return false; } for (int i = 0; i < other_parser.mBlockFilters.length; i++) { if (!other_parser.mBlockFilters[i].pattern().equals(this.mBlockFilters[i].pattern())) { return false; } } } if (null == other_parser.mValueFilters && this.mValueFilters != null || other_parser.mValueFilters != null && null == this.mValueFilters) { return false; } if (other_parser.mValueFilters != null && this.mValueFilters != null) { if (other_parser.mValueFilters.length != this.mValueFilters.length) { return false; } for (int i = 0; i < other_parser.mValueFilters.length; i++) { if (!other_parser.mValueFilters[i].pattern().equals(this.mValueFilters[i].pattern())) { return false; } } } return true; } public Parsed parse(String name, String encoding, TemplateTransformer transformer) throws TemplateException { if (null == name) throw new IllegalArgumentException("name can't be null."); URL resource = resolve(name); if (null == resource) { throw new TemplateNotFoundException(name, null); } return parse(prepare(name, resource), encoding, transformer); } public URL resolve(String name) { if (null == name) throw new IllegalArgumentException("name can't be null."); if (0 == name.indexOf(getPackage())) { name = name.substring(getPackage().length()); } name = name.replace('.', '/') + mExtension; URL resource = mTemplateFactory.getResourceFinder().getResource(name); if (null == resource) { resource = mTemplateFactory.getResourceFinder().getResource(DEFAULT_TEMPLATES_PATH + name); } return resource; } public String getPackage() { return mPackageName; } String escapeClassname(String name) { assert name != null; if (name.equals(mAmbiguousName)) { throw new AmbiguousTemplateNameException(name); } if (name.endsWith(mExtension)) { name = name.substring(0, name.length() - mExtensionLength); } char[] name_chars = name.toCharArray(); int char_code; for (int i = 0; i < name_chars.length; i++) { char_code = name_chars[i]; if ((char_code >= 48 && char_code <= 57) || (char_code >= 65 && char_code <= 90) || (char_code >= 97 && char_code <= 122) || char_code == 46) { continue; } if (char_code == '/' || char_code == '\\') { name_chars[i] = '.'; } else { name_chars[i] = '_'; } } return new String(name_chars); } public Parsed prepare(String name, URL resource) { if (null == name) throw new IllegalArgumentException("name can't be null."); if (null == resource) throw new IllegalArgumentException("resource can't be null."); String template_name = name; Parsed template_parsed = new Parsed(this); if (0 == template_name.indexOf(getPackage())) { template_name = name.substring(getPackage().length()); } String class_name = template_name; String subpackage = ""; int package_seperator = template_name.lastIndexOf("."); if (package_seperator != -1) { subpackage = "." + template_name.substring(0, package_seperator); class_name = template_name.substring(package_seperator + 1); } template_parsed.setTemplateName(template_name); template_parsed.setPackage(getPackage().substring(0, getPackage().length() - 1) + subpackage); template_parsed.setClassName(escapeClassname(class_name)); template_parsed.setResource(resource); return template_parsed; } Parsed parse(Parsed parsed, String encoding, TemplateTransformer transformer) throws TemplateException { assert parsed != null; if (null == encoding) { encoding = RifeConfig.Template.getDefaultEncoding(); } // get the resource of the template file URL resource = parsed.getResource(); // obtain the content of the template file StringBuilder content_buffer = getContent(parsed.getTemplateName(), parsed, resource, encoding, transformer); // replace the included templates Stack<String> previous_includes = new Stack<String>(); previous_includes.push(parsed.getFullClassName()); replaceIncludeTags(parsed, content_buffer, previous_includes, encoding, transformer); previous_includes.pop(); // process the blocks and values String content = content_buffer.toString(); parseBlocks(parsed, content); parsed.setFilteredBlocks(filterTags(mBlockFilters, parsed.getBlockIds())); parsed.setFilteredValues(filterTags(mValueFilters, parsed.getValueIds())); assert parsed.getBlocks().size() >= 1; return parsed; } private StringBuilder getContent(String templateName, Parsed parsed, URL resource, String encoding, TemplateTransformer transformer) throws TemplateException { if (null == transformer) { return getFileContent(resource, encoding); } else { return getTransformedContent(templateName, parsed, resource, encoding, transformer); } } private StringBuilder getFileContent(URL resource, String encoding) throws TemplateException { assert resource != null; String content = null; try { content = mTemplateFactory.getResourceFinder().getContent(resource, encoding); } catch (ResourceFinderErrorException e) { throw new GetContentErrorException(resource.toExternalForm(), e); } return new StringBuilder(content); } private StringBuilder getTransformedContent(String templateName, Parsed parsed, URL resource, String encoding, TemplateTransformer transformer) throws TemplateException { assert resource != null; ByteArrayOutputStream result = new ByteArrayOutputStream(); // transform the content transformer.setResourceFinder(mTemplateFactory.getResourceFinder()); Collection<URL> dependencies = transformer.transform(templateName, resource, result, encoding); // get the dependencies and their modification times if (dependencies != null && dependencies.size() > 0) { long modification_time = 0; for (URL dependency_resource : dependencies) { try { modification_time = transformer.getResourceFinder().getModificationTime(dependency_resource); } catch (ResourceFinderErrorException e) { // if there was trouble in obtaining the modification time, just set // it to 0 so that the dependency will always be outdated modification_time = 0; } if (modification_time > 0) { parsed.addDependency(dependency_resource, new Long(modification_time)); } } } // set the modification state so that different filter configurations // will reload the template, not only modifications to the dependencies parsed.setModificationState(transformer.getState()); // convert the result to a StringBuilder try { if (null == encoding) { if (null == transformer.getEncoding()) { return new StringBuilder(result.toString()); } else { return new StringBuilder(result.toString(transformer.getEncoding())); } } else { return new StringBuilder(result.toString(encoding)); } } catch (UnsupportedEncodingException e) { throw new TransformedResultConversionException(resource.toExternalForm(), e); } } public static long getModificationTime(ResourceFinder resourceFinder, URL resource) throws TemplateException { if (null == resource) throw new IllegalArgumentException("resource can't be null."); long modification_time = -1; try { modification_time = resourceFinder.getModificationTime(resource); } catch (ResourceFinderErrorException e) { throw new ModificationTimeErrorException(resource.toExternalForm(), e); } return modification_time; } private int forwardToNextWhitespace(int start, String content) { // forward until the first non whitespace character int i = start; for (; i < content.length(); i++) { if (!Character.isWhitespace(content.charAt(i))) { break; } } return i; } /** * Get the next indices of a search string in the content by taking * backslash escaping into account and skipping over those search * strings that are escaped by it. If fixed parts are provided it will also * ensure that the search string is followed by them and the text in between * is only whitespace. */ private TagMatch getEscapedIndex(String content, String search, int start, ConfigPart... fixedParts) { TagMatch match = new TagMatch(); int begin_index = content.indexOf(search, start); if (begin_index != -1) { int ending_index = -1; int last_match = begin_index; int last_ending_index = -1; while (begin_index != -1) { match.clear(); match.addMatch(begin_index); if (begin_index > 0 && '\\' == content.charAt(begin_index - 1) && // check for a first escaping backslash (begin_index < 2 || content.charAt(begin_index - 2) != '\\')) // check if that one hasn't been escaped itself { begin_index = -1; } // if fixed parts have been provided, ensure that they are available // after the last match and that the text between them is only // whitespace else if (fixedParts != null && fixedParts.length > 0) { last_ending_index = begin_index + search.length(); for (ConfigPart fixed_part : fixedParts) { if (0 == fixed_part.length()) { match.addMatch(forwardToNextWhitespace(last_ending_index, content)); match.addFixedPartMatched(true); continue; } ending_index = content.indexOf(fixed_part.getText(), last_ending_index); String seperating_text = null; String seperating_text_trim = null; if (ending_index != -1) { seperating_text = content.substring(last_ending_index, ending_index); seperating_text_trim = seperating_text.trim(); } if (-1 == ending_index || seperating_text_trim.length() != 0) { if (fixed_part.isOptional() && (null == seperating_text || seperating_text.indexOf(seperating_text_trim) > 0)) { match.addMatch(forwardToNextWhitespace(last_ending_index, content)); match.addFixedPartMatched(false); continue; } else { begin_index = -1; break; } } last_ending_index = ending_index + fixed_part.length(); match.addMatch(last_ending_index); match.addFixedPartMatched(true); } } // continue searching if the match wasn't successful if (-1 == begin_index) { begin_index = content.indexOf(search, last_match + 1); last_match = begin_index; continue; } return match; } } // return negative result return null; } /** * Construct an array with only the configurations that need to be unescaped */ private Parser.Config[] getUnescapeConfigs(String content) { // iterate over the supported parser configurations to find the ones // that are unused List<Parser.Config> configs_list = new ArrayList<Parser.Config>(); for (Parser.Config config : mConfigs) { if (content.indexOf(config.mUnescapeStart.getText()) != -1) { configs_list.add(config); } } Parser.Config[] configs = null; if (configs_list.size() > 0) { // make a config array, containing only the used configs configs = new Parser.Config[configs_list.size()]; configs_list.toArray(configs); } return configs; } /** * Removes the single escape backslash character from content */ private String unescapePart(Parser.Config[] configs, String part) { if (configs != null) { for (Parser.Config config : configs) { // minor optimization to only do regexp unescape matching when a // matching possibility is present through a quick lookup if (part.indexOf(config.mUnescapeStart.getText()) != -1) { part = config.mUnescapePattern.matcher(part).replaceAll("$1"); } } } part = removeTrailingDoubleEscape(part, 0, part.length()); return part; } /** * Replaces the double escaping backslashes from the end of block content * by a single backslash */ private String removeTrailingDoubleEscape(String content, int begin, int end) { if (end >= 2 && '\\' == content.charAt(end - 1) && '\\' == content.charAt(end - 2)) { return content.substring(begin, end - 1); } else { return content.substring(begin, end); } } /** * Looks for the first match of a config part text in the text content. If * a part doesn't match and is optional, the next one will be tried. If the * part is empty, the next one will always be tried immediately. **/ private ConfigPartMatch getFirstFoundPartIndex(String content, int startIndex, ConfigPart... parts) { for (ConfigPart part : parts) { if (part.length() != 0) { int result = content.indexOf(part.getText(), startIndex); if (result != -1) { return new ConfigPartMatch(result, part); } else if (!part.isOptional()) { return ConfigPartMatch.NO_MATCH; } } } return ConfigPartMatch.NO_MATCH; } private void replaceIncludeTags(Parsed parsed, StringBuilder content, Stack<String> previousIncludes, String encoding, TemplateTransformer transformer) throws TemplateException { assert parsed != null; assert content != null; assert previousIncludes != null; TagMatch tag_match = null; ConfigPartMatch part_match = null; int previous_index = 0; int begin_start_index = 0; int includename_end_index = 0; int term_start_index = 0; int tag_end_index = 0; int includename_begin_index = 0; boolean begin_isquoted = false; String included_template_name = null; URL included_template_resource = null; Parsed included_template_parsed = null; StringBuilder included_template_content = null; // process the included files // and iterate over the support parser configurations for (Parser.Config config : mConfigs) { do { // find the begin position of the include tag tag_match = getEscapedIndex(content.toString(), config.mTagStart.getText(), previous_index, config.mIncludeTag, config.mStringDelimiterBegin); if (tag_match != null) { begin_start_index = tag_match.getMatch(0); // find the begin of the filename includename_begin_index = tag_match.getMatch(2); begin_isquoted = tag_match.didFixedPartMatch(1); part_match = null; includename_end_index = -1; // find the string delimiter // get the string delimiter that marks the end of the value id int includename_end_offset = 0; if (begin_isquoted) { part_match = getFirstFoundPartIndex(content.toString(), includename_begin_index, config.mStringDelimiterEnd); } if (part_match != null && part_match.mPart != null) { includename_end_index = part_match.mIndex; includename_end_offset = part_match.mPart.length(); } // ensure that a name that started out quoted is already terminated with a delimiter else if (begin_isquoted && null == part_match.mPart) { throw new AttributeNotEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content.toString(), includename_begin_index), config.mIncludeTag.getText(), "name"); } else { int short_index = content.indexOf(config.mTagShortTerm.getText(), includename_begin_index); includename_end_offset = 0; includename_end_index = backtrackTillFirstNonWhitespace(content.toString(), short_index); } // check if the include name was ended if (-1 == includename_end_index) break; // obtain the filename included_template_name = content.substring(includename_begin_index, includename_end_index); // ensure that an end delimiter corresponds to a start delimiter if (!begin_isquoted && included_template_name.endsWith(config.mStringDelimiterEnd.getText())) { throw new AttributeWronglyEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content.toString(), includename_end_index + includename_end_offset - config.mStringDelimiterEnd.length()), config.mIncludeTag.getText(), "name"); } // get the first tag ending term_start_index = content.indexOf(config.mTagShortTerm.getText(), includename_end_index + includename_end_offset); // ensure that the tag was propertly ended if (-1 == term_start_index) break; // check that there's only whitespace between the delimiter and the start of the termination tag if (content.substring(includename_end_index + includename_end_offset, term_start_index).trim().length() > 0) { throw new TagBadlyTerminatedException(parsed.getClassName(), StringUtils.getDocumentPosition(content.toString(), term_start_index), config.mIncludeTag.getText(), included_template_name); } // calculate the index of the end of the complete tag tag_end_index = term_start_index+config.mTagShortTermLength; // obtain the parser that will be used to get the included content Parser include_parser = this; // check if the included template references another template type int doublecolon_index = included_template_name.indexOf(':'); if (doublecolon_index != -1) { String template_type = included_template_name.substring(0, doublecolon_index); if (!template_type.equals(mTemplateFactory.toString())) { TemplateFactory factory = TemplateFactory.getFactory(template_type); include_parser = factory.getParser(); included_template_name = included_template_name.substring(doublecolon_index + 1); } } included_template_resource = include_parser.resolve(included_template_name); if (null == included_template_resource) { throw new IncludeNotFoundException(parsed.getClassName(), StringUtils.getDocumentPosition(content.toString(), begin_start_index), included_template_name); } included_template_parsed = include_parser.prepare(included_template_name, included_template_resource); // check for circular references if (previousIncludes.contains(included_template_parsed.getFullClassName())) { throw new CircularIncludesException(parsed.getClassName(), StringUtils.getDocumentPosition(content.toString(), begin_start_index), included_template_name, previousIncludes); } // parse the included template's include tags too included_template_content = include_parser.getContent(included_template_name, parsed, included_template_parsed.getResource(), encoding, transformer); previousIncludes.push(included_template_parsed.getFullClassName()); replaceIncludeTags(included_template_parsed, included_template_content, previousIncludes, encoding, transformer); previousIncludes.pop(); // replace the tag with the included file's content // if there's a double escaping backslash before the tag, it // will also be unescaped if (begin_start_index >= 2 && '\\' == content.charAt(begin_start_index - 1) && '\\' == content.charAt(begin_start_index - 2)) { content.replace(begin_start_index - 1, tag_end_index, included_template_content.toString()); } else { content.replace(begin_start_index, tag_end_index, included_template_content.toString()); } // retain the link to this include file for optional later modification time checking parsed.addDependency(included_template_parsed); // add the dependencies of the included template too Map<URL, Long> included_dependencies = included_template_parsed.getDependencies(); for (Map.Entry<URL, Long> included_dependency : included_dependencies.entrySet()) { parsed.addDependency(included_dependency.getKey(), included_dependency.getValue()); } // continue the search after this tag previous_index = begin_start_index; } } while (tag_match != null); } } private TagMatch getFirstMatch(TagMatch... matches) { if (null == matches || 0 == matches.length) { return null; } TagMatch first_match = null; for (TagMatch match : matches) { if (match != null) { int candidate = match.getMatch(0); if (candidate != -1) { if (null == first_match) { first_match = match; } else { if (candidate <= first_match.getMatch(0)) { first_match = match; } } } } } return first_match; } private int getFirstMatch(int... candidates) { if (null == candidates || 0 == candidates.length) { return -1; } int first_candidate = -1; for (int candidate : candidates) { if (candidate != -1) { if (-1 == first_candidate) { first_candidate = candidate; } else { if (candidate <= first_candidate) { first_candidate = candidate; } } } } return first_candidate; } private int backtrackTillFirstNonWhitespace(String content, int index) { if (-1 == index) { return -1; } else { do { index--; if (!Character.isWhitespace(content.charAt(index))) { break; } } while (index >= 0); return index + 1; } } private void parseBlocks(Parsed parsed, String content) throws TemplateException { assert parsed != null; assert content != null; LinkedHashMap<String, StringBuilder> blocks = new LinkedHashMap<String, StringBuilder>(); blocks.put("", new StringBuilder("")); // iterate over the supported parser configurations to find the ones // that are unused List<Parser.Config> block_configs_list = new ArrayList<Parser.Config>(); for (Parser.Config config : mConfigs) { if (getEscapedIndex(content, config.mTagStart.getText(), 0, config.mBlockTag, config.mStringDelimiterBegin) != null || getEscapedIndex(content, config.mTagStart.getText(), 0, config.mBlockvalueTag, config.mStringDelimiterBegin) != null || getEscapedIndex(content, config.mTagStart.getText(), 0, config.mBlockappendTag, config.mStringDelimiterBegin) != null || getEscapedIndex(content, config.mTagTerm.getText(), 0, config.mBlockTag, config.mTagEnd) != null || getEscapedIndex(content, config.mTagTerm.getText(), 0, config.mBlockvalueTag, config.mTagEnd) != null || getEscapedIndex(content, config.mTagTerm.getText(), 0, config.mBlockappendTag, config.mTagEnd) != null || getEscapedIndex(content, config.mTagStart.getText(), 0, config.mCommentTag, config.mTagEnd) != null || getEscapedIndex(content, config.mTagTerm.getText(), 0, config.mCommentTag, config.mTagEnd) != null) { block_configs_list.add(config); } } Parser.Config[] configs = null; int previous_index = 0; if (block_configs_list.size() > 0) { // make a config array, containing only the used configs configs = new Parser.Config[block_configs_list.size()]; block_configs_list.toArray(configs); // setup the parser variables Stack<String> block_ids = new Stack<String>(); Stack<ConfigPart> block_types = new Stack<ConfigPart>(); Stack<Parser.Config> block_configs = new Stack<Parser.Config>(); // create the root block which is the anonymous main content block_ids.push(""); block_types.push(new ConfigPart(PartType.CONTENT, "CONTENT", false)); block_configs.push(null); TagMatch match1 = null; TagMatch match2 = null; TagMatch match3 = null; TagMatch match4 = null; TagMatch first_match = null; int begin_start_index = 0; int delimiter_end_index = 0; int begin_end_index = 0; int term_start_index = 0; int term_end_index = 0; int blockid_start_index = 0; int blockid_end_index = 0; int[] begin_start_indices = new int[configs.length]; int[] delimiter_end_indices = new int[configs.length]; boolean[] begin_isblockvalue_flags = new boolean[configs.length]; boolean[] begin_isblockappend_flags = new boolean[configs.length]; boolean[] begin_iscomment_flags = new boolean[configs.length]; boolean[] begin_isquoted_flags = new boolean[configs.length]; int[] term_start_indices = new int[configs.length]; int[] term_end_indices = new int[configs.length]; boolean[] term_isblockvalue_flags = new boolean[configs.length]; boolean[] term_isblockappend_flags = new boolean[configs.length]; boolean[] term_iscomment_flags = new boolean[configs.length]; String leftover_content = null; String blockid = null; Parser.Config config_begin = null; Parser.Config config_term = null; boolean begin_isblockvalue = false; boolean begin_isblockappend = false; boolean begin_iscomment = false; boolean begin_isquoted = false; boolean term_isblockvalue = false; boolean term_isblockappend = false; boolean term_iscomment = false; // process the block and content do { { String block_id_peek = block_ids.peek(); // iterate over the supported parser configurations to find all // start tag beginnings for (int i = 0; i < configs.length; i++) { if (null == block_id_peek) { first_match = null; } else { match1 = getEscapedIndex(content, configs[i].mTagStart.getText(), previous_index, configs[i].mBlockTag, configs[i].mStringDelimiterBegin); match2 = getEscapedIndex(content, configs[i].mTagStart.getText(), previous_index, configs[i].mBlockvalueTag, configs[i].mStringDelimiterBegin); match3 = getEscapedIndex(content, configs[i].mTagStart.getText(), previous_index, configs[i].mBlockappendTag, configs[i].mStringDelimiterBegin); match4 = getEscapedIndex(content, configs[i].mTagStart.getText(), previous_index, configs[i].mCommentTag, configs[i].mTagEnd); first_match = getFirstMatch(match1, match2, match3, match4); } if (null == first_match) { begin_start_indices[i] = -1; begin_isblockvalue_flags[i] = false; begin_isblockappend_flags[i] = false; begin_iscomment_flags[i] = false; begin_isquoted_flags[i] = false; delimiter_end_indices[i] = -1; } else { begin_start_indices[i] = first_match.getMatch(0); begin_isblockvalue_flags[i] = (first_match == match2); begin_isblockappend_flags[i] = (first_match == match3); begin_iscomment_flags[i] = (first_match == match4); begin_isquoted_flags[i] = first_match.didFixedPartMatch(1); delimiter_end_indices[i] = first_match.getMatch(2); } } // iterate over the supported parser configurations all term tag // beginnings for (int i = 0; i < configs.length; i++) { if (null == block_id_peek) { match1 = null; match2 = null; match3 = null; } else { match1 = getEscapedIndex(content, configs[i].mTagTerm.getText(), previous_index, configs[i].mBlockTag, configs[i].mTagEnd); match2 = getEscapedIndex(content, configs[i].mTagTerm.getText(), previous_index, configs[i].mBlockvalueTag, configs[i].mTagEnd); match3 = getEscapedIndex(content, configs[i].mTagTerm.getText(), previous_index, configs[i].mBlockappendTag, configs[i].mTagEnd); } match4 = getEscapedIndex(content, configs[i].mTagTerm.getText(), previous_index, configs[i].mCommentTag, configs[i].mTagEnd); first_match = getFirstMatch(match1, match2, match3, match4); if (null == first_match) { term_start_indices[i] = -1; term_isblockvalue_flags[i] = false; term_isblockappend_flags[i] = false; term_iscomment_flags[i] = false; term_end_indices[i] = -1; } else { term_start_indices[i] = first_match.getMatch(0); term_isblockvalue_flags[i] = (first_match == match2); term_isblockappend_flags[i] = (first_match == match3); term_iscomment_flags[i] = (first_match == match4); term_end_indices[i] = first_match.getMatch(2); } } } // find the start position of the next block begin tags by comparing // which is the earliest start tag beginning config_begin = null; begin_start_index = -1; for (int i = 0; i < begin_start_indices.length; i++) { if (begin_start_indices[i] != -1 && (-1 == begin_start_index || begin_start_indices[i] < begin_start_index)) { begin_start_index = begin_start_indices[i]; begin_isblockvalue = begin_isblockvalue_flags[i]; begin_isblockappend = begin_isblockappend_flags[i]; begin_iscomment = begin_iscomment_flags[i]; begin_isquoted = begin_isquoted_flags[i]; delimiter_end_index = delimiter_end_indices[i]; config_begin = configs[i]; } } // find the start position of the next block term tags by comparing // which is the earliest term tag beginning config_term = null; term_start_index = -1; term_end_index = -1; for (int i = 0; i < term_start_indices.length; i++) { if (term_start_indices[i] != -1 && (-1 == term_start_index || term_start_indices[i] < term_start_index)) { term_start_index = term_start_indices[i]; term_isblockvalue = term_isblockvalue_flags[i]; term_isblockappend = term_isblockappend_flags[i]; term_iscomment = term_iscomment_flags[i]; term_end_index = term_end_indices[i]; config_term = configs[i]; } } // check if a begin tag was found and if the end tag comes after it // or the end tag has been omitted which means that the blocks are nested // start the new block that corresponds to the new begin tag if (begin_start_index != -1 && (begin_start_index < term_start_index || -1 == term_start_index)) { // the text upto the beginning of this block has to be extracted // check if the next begin tag has been double-escaped leftover_content = removeTrailingDoubleEscape(content, previous_index, begin_start_index); // and added to the preceeding block blockid = block_ids.peek(); if (blockid != null) { blocks.get(blockid).append(leftover_content); } // check if this is a comment tag and don't extract an id in that case if (begin_iscomment) { blockid_start_index = -1; blockid_end_index = -1; begin_end_index = delimiter_end_index; blockid = null; // add this comment block to the stack of active blocks block_ids.push(null); block_types.push(config_begin.mCommentTag); block_configs.push(config_begin); } else { // get the begin and end position of the block's id blockid_start_index = delimiter_end_index; // get the string delimiter that marks the end of the block id ConfigPartMatch delimiter_end_match = null; if (begin_isquoted) { delimiter_end_match = getFirstFoundPartIndex(content, blockid_start_index, config_begin.mStringDelimiterEnd); } if (delimiter_end_match != null && delimiter_end_match.mPart != null) { blockid_end_index = delimiter_end_match.mIndex; } // ensure that a name that started out quoted is already terminated with a delimiter else if (begin_isquoted && null == delimiter_end_match.mPart) { String tag_type; if (begin_isblockvalue) tag_type = config_begin.mBlockvalueTag.getText(); else if (begin_isblockappend) tag_type = config_begin.mBlockappendTag.getText(); else tag_type = config_begin.mBlockTag.getText(); throw new AttributeNotEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, blockid_start_index), tag_type, "name"); } else { int long_index = content.indexOf(config_begin.mTagEnd.getText(), blockid_start_index); int short_index = content.indexOf(config_begin.mTagShortTerm.getText(), blockid_start_index); // backtrack until the first non whitespace character blockid_end_index = backtrackTillFirstNonWhitespace(content, getFirstMatch(long_index, short_index)); } if (-1 == blockid_end_index) { String tag_type; if (begin_isblockvalue) tag_type = config_begin.mBlockvalueTag.getText(); else if (begin_isblockappend) tag_type = config_begin.mBlockappendTag.getText(); else tag_type = config_begin.mBlockTag.getText(); throw new AttributeNotEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, blockid_start_index), tag_type, "name"); } else { // extract the id of the block blockid = content.substring(blockid_start_index, blockid_end_index); // ensure that an end delimiter corresponds to a start delimiter if (!begin_isquoted && blockid.endsWith(config_begin.mStringDelimiterEnd.getText())) { String tag_type; if (begin_isblockvalue) tag_type = config_begin.mBlockvalueTag.getText(); else if (begin_isblockappend) tag_type = config_begin.mBlockappendTag.getText(); else tag_type = config_begin.mBlockTag.getText(); throw new AttributeWronglyEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, blockid_end_index - config_begin.mStringDelimiterEnd.length()), tag_type, "name"); } // get the index of the end of the block begin tag begin_end_index = content.indexOf(config_begin.mTagEnd.getText(), blockid_end_index); if (-1 == begin_end_index) { String tag_type; if (begin_isblockvalue) tag_type = config_begin.mBlockvalueTag.getText(); else if (begin_isblockappend) tag_type = config_begin.mBlockappendTag.getText(); else tag_type = config_begin.mBlockTag.getText(); throw new BeginTagNotEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, blockid_end_index), tag_type, blockid); } else { // check that the begin tag termination is valid if (delimiter_end_match != null && delimiter_end_match.mPart != null && content.substring(blockid_end_index + delimiter_end_match.mPart.length(), begin_end_index).trim().length() > 0) { String tag_type; if (begin_isblockvalue) tag_type = config_begin.mBlockvalueTag.getText(); else if (begin_isblockappend) tag_type = config_begin.mBlockappendTag.getText(); else tag_type = config_begin.mBlockTag.getText(); throw new BeginTagBadlyTerminatedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, blockid_end_index + config_begin.mStringDelimiterEnd.length()), tag_type, blockid); } // the start tag was correctly ended, add this block to the stack of active block ids block_ids.push(blockid); block_configs.push(config_begin); if (begin_isblockvalue) { blocks.put(blockid, new StringBuilder("")); block_types.push(config_begin.mBlockvalueTag); parsed.setBlockvalue(blockid); } else if (begin_isblockappend) { StringBuilder current_block = blocks.get(blockid); if (null == current_block) { current_block = new StringBuilder(""); blocks.put(blockid, current_block); } block_types.push(config_begin.mBlockappendTag); parsed.setBlockvalue(blockid); } else { blocks.put(blockid, new StringBuilder("")); block_types.push(config_begin.mBlockTag); } } } } // continue the search after this tag previous_index = begin_end_index + config_begin.mTagEndLength; } // the termination tag came before the end tag, this means that the current block first has // to be closed correctly else if (term_start_index > -1) { // the text upto the termination tag of the current block has to be extracted // check if the next termination tag has been double-escaped leftover_content = removeTrailingDoubleEscape(content, previous_index, term_start_index); // check if a tag is actually open if (1 == block_ids.size()) { String class_name = parsed.getClassName(); DocumentPosition doc_position = StringUtils.getDocumentPosition(content, term_start_index); if (term_isblockvalue) throw new TerminatingUnopenedTagException(class_name, doc_position, config_term.mBlockvalueTag.getText()); else if (term_isblockappend) throw new TerminatingUnopenedTagException(class_name, doc_position, config_term.mBlockappendTag.getText()); else if (term_iscomment) throw new TerminatingUnopenedTagException(class_name, doc_position, config_term.mCommentTag.getText()); else throw new TerminatingUnopenedTagException(class_name, doc_position, config_term.mBlockTag.getText()); } // and added to the content of the current block blockid = block_ids.peek(); if (blockid != null) { blocks.get(blockid).append(leftover_content); PartType block_type = block_types.peek().getType(); // ensure that blockvalue tags are closed with the appropriate termination tag if (PartType.BLOCKVALUE == block_type) { if (term_iscomment) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockvalueTag.getText(), config_term.mCommentTag.getText()); } else if (term_isblockappend) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockvalueTag.getText(), config_term.mBlockappendTag.getText()); } else if (!term_isblockvalue) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockvalueTag.getText(), config_term.mBlockTag.getText()); } } // ensure that blockappend tags are closed with the appropriate termination tag else if (PartType.BLOCKAPPEND == block_type) { if (term_iscomment) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockappendTag.getText(), config_term.mCommentTag.getText()); } else if (term_isblockvalue) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockappendTag.getText(), config_term.mBlockvalueTag.getText()); } else if (!term_isblockappend) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockappendTag.getText(), config_term.mBlockTag.getText()); } } // ensure that the block tags are closed with the appropriate termination tag else { if (term_iscomment) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockTag.getText(), config_term.mCommentTag.getText()); } else if (term_isblockvalue) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockTag.getText(), config_term.mBlockvalueTag.getText()); } else if (term_isblockappend) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mBlockTag.getText(), config_term.mBlockappendTag.getText()); } } } // ensure that the comments tags are closed with the appropriate termination tag else if (!term_iscomment) { if (term_isblockvalue) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mCommentTag.getText(), config_term.mBlockvalueTag.getText()); } else if (term_isblockappend) { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mCommentTag.getText(), config_term.mBlockappendTag.getText()); } else { throw new MismatchedTerminationTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), blockid, config_term.mCommentTag.getText(), config_term.mBlockTag.getText()); } } // the block has been terminated, remove its id from the stack block_ids.pop(); block_types.pop(); block_configs.pop(); // continue the search after this tag previous_index = term_end_index; } } // continue until no start and end tags are found anymore while (begin_start_index > -1 || term_start_index > -1); // check if all tags were correctly closed if (block_ids.size() > 1) { throw new MissingTerminationTagsException(parsed.getClassName(), StringUtils.getDocumentPosition(content, content.length()), block_types.peek().getText()); } } // append everything that's after the last block to the general content blocks.get("").append(content.substring(previous_index)); // iterate over the supported parser configurations to find the ones // that are unused for value tags List<Parser.Config> value_configs_list = new ArrayList<Parser.Config>(); for (Parser.Config config : mConfigs) { if (getEscapedIndex(content, config.mTagStart.getText(), 0, config.mValueTag, config.mStringDelimiterBegin) != null || getEscapedIndex(content, config.mTagTerm.getText(), 0, config.mValueTag, config.mTagEnd) != null) { value_configs_list.add(config); } } Parser.Config[] value_configs = null; if (value_configs_list.size() > 0) { // make a config array, containing only the used configs value_configs = new Parser.Config[value_configs_list.size()]; value_configs_list.toArray(value_configs); } Parser.Config[] unescape_configs = getUnescapeConfigs(content); // chop the blocks into block parts according to the value tags that are found in each block for (String block_key : blocks.keySet()) { parsed.setBlock(block_key, parseBlockParts(value_configs, unescape_configs, parsed, blocks.get(block_key).toString())); } } private ParsedBlockData parseBlockParts(Parser.Config[] configs, Parser.Config[] unescapeConfigs, Parsed parsed, String content) throws TemplateException { assert parsed != null; assert content != null; ParsedBlockData block_data = new ParsedBlockData(); int previous_index = 0; if (configs != null) { // setup the parser variables TagMatch match = null; int begin_start_index = 0; boolean begin_isquoted = false; int term_start_index = 0; int term_end_index = 0; int valueid_start_index = 0; int valueid_end_index = 0; int begin_term_index = 0; int[] begin_start_indices = new int[configs.length]; boolean[] begin_isquoted_flags = new boolean[configs.length]; int[] delimiter_end_indices = new int[configs.length]; int[] term_start_indices = new int[configs.length]; int[] term_end_indices = new int[configs.length]; String valueid = null; String valuetag_start = null; Parser.Config config_begin = null; Parser.Config config_term = null; // extracts all the parts that make up a block do { // iterate over the supported parser configurations to find all // start tag beginnings for (int i = 0; i < configs.length; i++) { match = getEscapedIndex(content, configs[i].mTagStart.getText(), previous_index, configs[i].mValueTag, configs[i].mStringDelimiterBegin); if (null == match) { begin_start_indices[i] = -1; begin_isquoted_flags[i] = false; delimiter_end_indices[i] = -1; } else { begin_start_indices[i] = match.getMatch(0); begin_isquoted_flags[i] = match.didFixedPartMatch(1); delimiter_end_indices[i] = match.getMatch(2); } } // find the start position of the next value begin tags by comparing // which is the earliest start tag beginning config_begin = null; begin_start_index = -1; begin_isquoted = false; for (int i = 0; i < begin_start_indices.length; i++) { if (begin_start_indices[i] != -1 && (-1 == begin_start_index || begin_start_indices[i] < begin_start_index)) { begin_start_index = begin_start_indices[i]; begin_isquoted = begin_isquoted_flags[i]; valueid_start_index = delimiter_end_indices[i]; config_begin = configs[i]; } } // check if a begin tag was found if (-1 != begin_start_index) { // add the text up to the beginning of this value tag to the list of parts if (previous_index < begin_start_index) { String part = unescapePart(unescapeConfigs, content.substring(previous_index, begin_start_index)); block_data.addPart(new ParsedBlockText(part)); } // get the string delimiter that marks the end of the value id ConfigPartMatch delimiter_end_match = null; int valueid_end_offset = 0; if (begin_isquoted) { delimiter_end_match = getFirstFoundPartIndex(content, valueid_start_index, config_begin.mStringDelimiterEnd); } if (delimiter_end_match != null && delimiter_end_match.mPart != null) { valueid_end_index = delimiter_end_match.mIndex; valueid_end_offset = delimiter_end_match.mPart.length(); } // ensure that a name that started out quoted is already terminated with a delimiter else if (begin_isquoted && null == delimiter_end_match.mPart) { throw new AttributeNotEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, valueid_start_index), config_begin.mValueTag.getText(), "name"); } else { int long_index = content.indexOf(config_begin.mTagEnd.getText(), valueid_start_index); int short_index = content.indexOf(config_begin.mTagShortTerm.getText(), valueid_start_index); int first_index = getFirstMatch(long_index, short_index); valueid_end_offset = 0; valueid_end_index = backtrackTillFirstNonWhitespace(content, first_index); } // check if the value id was ended if (-1 == valueid_end_index) { throw new AttributeNotEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, valueid_start_index), config_begin.mValueTag.getText(), "name"); } // extract the appearance of the value tag start so that it can be reused later // when the value hasn't been set in the template valuetag_start = content.substring(begin_start_index, valueid_start_index); // extract the id of the value and store it valueid = content.substring(valueid_start_index, valueid_end_index); // ensure that an end delimiter corresponds to a start delimiter if (!begin_isquoted && valueid.endsWith(config_begin.mStringDelimiterEnd.getText())) { throw new AttributeWronglyEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, valueid_end_index + valueid_end_offset - config_begin.mStringDelimiterEnd.length()), config_begin.mValueTag.getText(), "name"); } // add the value ID to the list of parsed values parsed.addValue(valueid); // get the first tag ending begin_term_index = content.indexOf(config_begin.mTagEnd.getText(), valueid_end_index + valueid_end_offset); // ensure that the tag was propertly ended if (-1 == begin_term_index) { throw new BeginTagNotEndedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, valueid_end_index + valueid_end_offset), config_begin.mValueTag.getText(), valueid); } // check if it is short value tag int begin_short_term_index = begin_term_index - (config_begin.mTagShortTermLength - config_begin.mTagEndLength); if (config_begin.mTagShortTerm.equals(content.substring(begin_short_term_index, begin_short_term_index + config_begin.mTagShortTermLength))) { // check that the begin tag termination is valid if (content.substring(valueid_end_index + valueid_end_offset, begin_short_term_index).trim().length() > 0) { throw new BeginTagBadlyTerminatedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, valueid_end_index + valueid_end_offset), config_begin.mValueTag.getText(), valueid); } term_start_index = begin_short_term_index; // add the value part block_data.addPart(new ParsedBlockValue(valueid, content.substring(begin_start_index, begin_short_term_index + config_begin.mTagShortTermLength))); // continue the search after this tag previous_index = term_start_index + config_begin.mTagShortTermLength; } // check if it was a long value tag else { // check that the begin tag termination is valid if (content.substring(valueid_end_index + valueid_end_offset, begin_term_index).trim().length() > 0) { throw new BeginTagBadlyTerminatedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, valueid_end_index + valueid_end_offset), config_begin.mValueTag.getText(), valueid); } // iterate over the supported parser configurations all term tag // beginnings for (int i = 0; i < configs.length; i++) { match = getEscapedIndex(content, configs[i].mTagTerm.getText(), previous_index, configs[i].mValueTag, configs[i].mTagEnd); if (null == match) { term_start_indices[i] = -1; term_end_indices[i] = -1; } else { term_start_indices[i] = match.getMatch(0); term_end_indices[i] = match.getMatch(2); } } // find the start position of the next value term tags by comparing // which is the earliest term tag beginning config_term = null; term_start_index = -1; for (int i = 0; i < term_start_indices.length; i++) { if (term_start_indices[i] != -1 && (-1 == term_start_index || term_start_indices[i] < term_start_index)) { term_start_index = term_start_indices[i]; term_end_index = term_end_indices[i]; config_term = configs[i]; } } // the termination tag always has to be found if a begin tag was found if (-1 == term_start_index) { throw new TagNotTerminatedException(parsed.getClassName(), StringUtils.getDocumentPosition(content, begin_start_index), config_begin.mValueTag.getText(), valueid); } else { // iterate over all configuration to check if none introduce a nested value tag for (Config config_tmp : configs) { // get the start of the next begin value tag to check for nested value tags match = getEscapedIndex(content, config_tmp.mTagStart.getText(), valueid_start_index, config_tmp.mValueTag, config_tmp.mStringDelimiterBegin); // nested value tags are not permitted if (match != null && match.getMatch(0) < term_start_index) { throw new UnsupportedNestedTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, match.getMatch(0)), config_tmp.mValueTag.getText(), valueid); } } // check if an unopened value tag isn't being closed if (begin_term_index > term_start_index) { throw new TerminatingUnopenedTagException(parsed.getClassName(), StringUtils.getDocumentPosition(content, term_start_index), config_term.mValueTag.getText()); } // the termination tag comes after the begin tag and the next begin tag after the termination tag parsed.setDefaultValue(valueid, unescapePart(unescapeConfigs, content.substring(begin_term_index + config_begin.mTagEndLength, term_start_index))); // add the value part block_data.addPart(new ParsedBlockValue(valueid, valuetag_start + valueid + config_term.mStringDelimiterEnd + config_term.mTagShortTerm)); // continue the search after this tag previous_index = term_end_index; } } } } // continue until no start and end tags are found anymore while (begin_start_index > -1 && term_start_index > -1); } // append everything that's after the last value as a text block part if (previous_index < content.length()) { block_data.addPart(new ParsedBlockText(unescapePart(unescapeConfigs, content.substring(previous_index)))); } return block_data; } private FilteredTagsMap filterTags(Pattern[] filters, Collection<String> tags) { FilteredTagsMap result = null; if (filters != null) { result = new FilteredTagsMap(); Matcher filter_matcher = null; ArrayList<String> captured_groups = null; String pattern = null; String[] captured_groups_array = null; ArrayList<String> filtered_tags = new ArrayList<String>(); // iterate over the tag filters for (Pattern filter_pattern : filters) { // go over all the tags and try to match them against the current filter for (String tag : tags) { // skip over tags that have already been filtered if (filtered_tags.contains(tag)) { continue; } // create the filter matcher filter_matcher = filter_pattern.matcher(tag); // if the filter matches, and it returned capturing groups, // add the returned groups to the filtered tag mapping while (filter_matcher.find()) { if (null == captured_groups) { captured_groups = new ArrayList<String>(); captured_groups.add(tag); } if (filter_matcher.groupCount() > 0) { // store the captured groups for (int j = 1; j <= filter_matcher.groupCount(); j++) { captured_groups.add(filter_matcher.group(j)); } } } if (captured_groups != null) { pattern = filter_pattern.pattern(); captured_groups_array = new String[captured_groups.size()]; captured_groups.toArray(captured_groups_array); result.addFilteredTag(pattern, captured_groups_array); filtered_tags.add(tag); captured_groups_array = null; captured_groups = null; } } } } return result; } static class TagMatch { private ArrayIntList mMatches = null; private ArrayIntList mFixedPartsMatched = null; void addMatch(int match) { if (null == mMatches) { mMatches = new ArrayIntList(); } mMatches.add(match); } int getMatch(int index) { if (null == mMatches || index >= mMatches.size()) { return -1; } return mMatches.get(index); } void addFixedPartMatched(boolean match) { if (null == mFixedPartsMatched) { mFixedPartsMatched = new ArrayIntList(); } mFixedPartsMatched.add(match ? 1 : 0); } boolean didFixedPartMatch(int index) { if (null == mFixedPartsMatched || index >= mFixedPartsMatched.size()) { return false; } return 1 == mFixedPartsMatched.get(index); } void clear() { if (mMatches != null) { mMatches.clear(); } if (mFixedPartsMatched != null) { mFixedPartsMatched.clear(); } } } static class ConfigPartMatch { static final ConfigPartMatch NO_MATCH = new ConfigPartMatch(-1, null); private int mIndex = -1; private ConfigPart mPart = null; ConfigPartMatch(int index, ConfigPart part) { mIndex = index; mPart = part; } int length() { if (null == mPart) { return 0; } return mPart.length(); } } public static class MandatoryConfigPart extends ConfigPart { public MandatoryConfigPart(PartType type, String text) { super(type, text, false); } } public static class OptionalConfigPart extends ConfigPart { public OptionalConfigPart(PartType type, String text) { super(type, text, true); } } public static class ConfigPart implements CharSequence { private PartType mType; private String mText; private boolean mOptional; private ConfigPart(PartType type, String text, boolean optional) { mType = type; if (null == text) { text = ""; } mText = text; mOptional = optional; } public PartType getType() { return mType; } public String getText() { return mText; } public boolean isOptional() { return mOptional; } public int length() { return mText.length(); } public char charAt(int index) { return mText.charAt(index); } public CharSequence subSequence(int start, int end) { return mText.subSequence(start, end); } public String toString() { return mText; } public boolean equals(Object other) { if (null == other) { return false; } if (other == this) { return true; } if (other instanceof CharSequence) { return mText.equals(other); } if (!(other instanceof ConfigPart)) { return false; } ConfigPart other_part = (ConfigPart)other; return mType == other_part.mType && mOptional == other_part.mOptional && mText.equals(other_part.mText); } public int hashCode() { return mType.hashCode() * mText.hashCode() * (mOptional ? 1 : 0); } } static class Config implements Cloneable { private ConfigPart mTagStart = null; private ConfigPart mTagEnd = null; private ConfigPart mTagTerm = null; private ConfigPart mTagShortTerm = null; private ConfigPart mStringDelimiterBegin = null; private ConfigPart mStringDelimiterEnd = null; private ConfigPart mValueTag = null; private ConfigPart mBlockTag = null; private ConfigPart mBlockvalueTag = null; private ConfigPart mBlockappendTag = null; private ConfigPart mIncludeTag = null; private ConfigPart mCommentTag = null; private ConfigPart mUnescapeStart = null; private Pattern mUnescapePattern = null; private int mTagEndLength = -1; private int mTagTermLength = -1; private int mTagShortTermLength = -1; Config(String tagStart, String tagEnd, String tagTerm, String tagShortTerm, ConfigPart stringDelimiterBegin, ConfigPart stringDelimiterEnd, String valueTag, String blockTag, String blockvalueTag, String blockappendTag, String includeTag, String commentTag) { assert tagStart != null; assert tagEnd != null; assert stringDelimiterBegin != null; assert stringDelimiterEnd != null; assert tagTerm != null; assert tagShortTerm != null; assert valueTag != null; assert blockTag != null; assert blockvalueTag != null; assert blockappendTag != null; assert includeTag != null; assert commentTag != null; mTagStart = new MandatoryConfigPart(PartType.TAG_START, tagStart); mTagEnd = new MandatoryConfigPart(PartType.TAG_END, tagEnd); mStringDelimiterBegin = stringDelimiterBegin; mStringDelimiterEnd = stringDelimiterEnd; mValueTag = new MandatoryConfigPart(PartType.VALUE, valueTag); mBlockTag = new MandatoryConfigPart(PartType.BLOCK, blockTag); mBlockvalueTag = new MandatoryConfigPart(PartType.BLOCKVALUE, blockvalueTag); mBlockappendTag = new MandatoryConfigPart(PartType.BLOCKAPPEND, blockappendTag); mIncludeTag = new MandatoryConfigPart(PartType.INCLUDE, includeTag); mCommentTag = new MandatoryConfigPart(PartType.COMMENT, commentTag); mTagTerm = new MandatoryConfigPart(PartType.TAG_TERM, tagTerm); mTagShortTerm = new MandatoryConfigPart(PartType.TAG_SHORT_TERM, tagShortTerm); mUnescapeStart = new MandatoryConfigPart(PartType.UNESCAPE_START, "\\" + tagStart); mUnescapePattern = Pattern.compile("\\\\((?:\\Q" + tagStart + "\\E|\\Q" + mTagTerm + "\\E)\\s*(?:" + mIncludeTag + "|" + mBlockTag + "|" + mBlockvalueTag + "|" + mBlockappendTag + "|" + mValueTag + "|" + mCommentTag + "))"); mTagEndLength = mTagEnd.length(); mTagTermLength = mTagTerm.length(); mTagShortTermLength = mTagShortTerm.length(); assert mTagTerm != null; assert mTagShortTerm != null; assert mStringDelimiterBegin != null; assert mStringDelimiterEnd != null; assert mTagEndLength > 0; assert mTagTermLength > 0; assert mTagShortTermLength > 0; } public Config clone() { Config new_parserconfig = null; try { new_parserconfig = (Config)super.clone(); } catch (CloneNotSupportedException e) { new_parserconfig = null; } return new_parserconfig; } public boolean equals(Object object) { if (object == this) { return true; } if (null == object) { return false; } if (!(object instanceof Config)) { return false; } Config other_parserconfig = (Config)object; if (other_parserconfig.mTagStart.equals(this.mTagStart) && other_parserconfig.mTagEnd.equals(this.mTagEnd) && other_parserconfig.mTagTerm.equals(this.mTagTerm) && other_parserconfig.mTagShortTerm.equals(this.mTagShortTerm) && other_parserconfig.mStringDelimiterBegin.equals(this.mStringDelimiterBegin) && other_parserconfig.mStringDelimiterEnd.equals(this.mStringDelimiterEnd) && other_parserconfig.mValueTag.equals(this.mValueTag) && other_parserconfig.mBlockTag.equals(this.mBlockTag) && other_parserconfig.mBlockvalueTag.equals(this.mBlockvalueTag) && other_parserconfig.mBlockappendTag.equals(this.mBlockappendTag) && other_parserconfig.mIncludeTag.equals(this.mIncludeTag) && other_parserconfig.mCommentTag.equals(this.mCommentTag)) { return true; } else { return false; } } } }