package org.kefirsf.bb; import org.kefirsf.bb.conf.*; import org.kefirsf.bb.util.Utils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.text.MessageFormat; import java.util.*; /** * Create a text processor configuration from the DOM-document. * * @author Vitaliy Samolovskih aka Kefir */ public class DomConfigurationFactory { /** * Schema location */ private static final String SCHEMA_LOCATION = "http://kefirsf.org/kefirbb/schema"; /** * Constants which uses when parse XML-configuration */ private static final String TAG_CODE = "code"; private static final String TAG_CODE_ATTR_NAME = "name"; private static final String TAG_CODE_ATTR_PRIORITY = "priority"; private static final String TAG_CODE_ATTR_TRANSPARENT = "transparent"; private static final String TAG_PATTERN = "pattern"; private static final String TAG_VAR = "var"; private static final String TAG_VAR_ATTR_NAME = "name"; private static final String TAG_VAR_ATTR_PARSE = "parse"; private static final boolean DEFAULT_PARSE_VALUE = true; private static final String TAG_VAR_ATTR_INHERIT = "inherit"; private static final boolean DEFAULT_INHERIT_VALUE = false; private static final String TAG_VAR_ATTR_REGEX = "regex"; private static final String TAG_VAR_ATTR_TRANSPARENT = "transparent"; private static final String TAG_VAR_ATTR_FUNCTION = "function"; private static final String TAG_VAR_ATTR_ACTION = "action"; private static final String TAG_TEMPLATE = "template"; private static final String TAG_SCOPE = "scope"; private static final String TAG_SCOPE_ATTR_NAME = "name"; private static final String TAG_SCOPE_ATTR_PARENT = "parent"; private static final String TAG_SCOPE_ATTR_STRONG = "strong"; private static final String TAG_SCOPE_ATTR_IGNORE_TEXT = "ignoreText"; private static final String TAG_SCOPE_ATTR_MAX = "max"; private static final String TAG_SCOPE_ATTR_MIN = "min"; private static final String TAG_CODEREF = "coderef"; private static final String TAG_CODEREF_ATTR_NAME = TAG_CODE_ATTR_NAME; private static final String TAG_PREFIX = "prefix"; private static final String TAG_SUFFIX = "suffix"; private static final String TAG_PARAMS = "params"; private static final String TAG_PARAM = "param"; private static final String TAG_PARAM_ATTR_NAME = "name"; private static final String TAG_PARAM_ATTR_VALUE = "value"; private static final String TAG_CONSTANT = "constant"; private static final String TAG_CONSTANT_ATTR_VALUE = "value"; private static final String TAG_CONSTANT_ATTR_IGNORE_CASE = "ignoreCase"; private static final String TAG_JUNK = "junk"; private static final String TAG_EOL = "eol"; private static final String TAG_BOL = "bol"; private static final String TAG_BLANKLINE = "blankline"; private static final String TAG_NESTING = "nesting"; private static final String TAG_NESTING_ATTR_LIMIT = "limit"; private static final String TAG_NESTING_ATTR_EXCEPTION = "exception"; private static final String TAG_ATTR_GHOST = "ghost"; private static final String TAG_URL = "url"; private static final String TAG_URL_ATTR_LOCAL = "local"; private static final String TAG_URL_ATTR_SCHEMALESS = "schemaless"; private static final String TAG_IF = "if"; private static final String TAG_EMAIL = "email"; /** * Instance of the class. */ private static final DomConfigurationFactory instance = new DomConfigurationFactory(); /** * Private constructor for prevent class initialization. */ private DomConfigurationFactory() { } /** * @return factory instance */ public static DomConfigurationFactory getInstance() { return instance; } /** * Create the bb-code processor from DOM Document * * @param dc document * @return bb-code processor * @throws TextProcessorFactoryException If invalid Document */ public Configuration create(Document dc) { // Create configuration Configuration configuration = new Configuration(); parseNesting(configuration, dc); // Parse parameters configuration.setParams(parseParams(dc)); // Parse prefix and suffix configuration.setPrefix(parseFix(dc, TAG_PREFIX)); configuration.setSuffix(parseFix(dc, TAG_SUFFIX)); // Parse codes and scope and set this to configuration // Parse scopes NodeList scopeNodeList = dc.getDocumentElement().getElementsByTagNameNS(SCHEMA_LOCATION, TAG_SCOPE); Map<String, Scope> scopes = parseScopes(scopeNodeList); boolean fillRoot = false; Scope root; if (!scopes.containsKey(Scope.ROOT)) { root = new Scope(Scope.ROOT); scopes.put(Scope.ROOT, root); fillRoot = true; } else { root = scopes.get(Scope.ROOT); } // Parse codes Map<String, Code> codes = parseCodes(dc, scopes); // include codes in scopes fillScopeCodes(scopeNodeList, scopes, codes); // If root scope not defined in configuration file, then root scope fills all codes if (fillRoot) { root.setCodes(new HashSet<Code>(codes.values())); } // set root scope configuration.setRootScope(root); // return configuration return configuration; } /** * Parse nesting element, which describes nesting behavior. * * @param configuration parser configuration * @param dc DOM-document */ private void parseNesting(Configuration configuration, Document dc) { NodeList list = dc.getElementsByTagNameNS(SCHEMA_LOCATION, TAG_NESTING); if (list.getLength() > 0) { Node el = list.item(0); configuration.setNestingLimit(nodeAttribute(el, TAG_NESTING_ATTR_LIMIT, Configuration.DEFAULT_NESTING_LIMIT)); configuration.setPropagateNestingException( nodeAttribute(el, TAG_NESTING_ATTR_EXCEPTION, Configuration.DEFAULT_PROPAGATE_NESTING_EXCEPTION) ); } } /** * Parse configuration predefined parameters. * * @param dc DOM-document * @return parameters */ private Map<String, CharSequence> parseParams(Document dc) { Map<String, CharSequence> params = new HashMap<String, CharSequence>(); NodeList paramsElements = dc.getElementsByTagNameNS(SCHEMA_LOCATION, TAG_PARAMS); if (paramsElements.getLength() > 0) { Element paramsElement = (Element) paramsElements.item(0); NodeList paramElements = paramsElement.getElementsByTagNameNS(SCHEMA_LOCATION, TAG_PARAM); for (int i = 0; i < paramElements.getLength(); i++) { Node paramElement = paramElements.item(i); String name = nodeAttribute(paramElement, TAG_PARAM_ATTR_NAME, ""); String value = nodeAttribute(paramElement, TAG_PARAM_ATTR_VALUE, ""); if (name != null && name.length() > 0) { params.put(name, value); } } } return params; } /** * Parse prefix or suffix. * * @param dc DOM-document. * @param tagname tag name. * @return template. */ private Template parseFix(Document dc, String tagname) { Template fix; NodeList prefixElementList = dc.getElementsByTagNameNS(SCHEMA_LOCATION, tagname); if (prefixElementList.getLength() > 0) { fix = parseTemplate(prefixElementList.item(0)); } else { fix = new Template(); } return fix; } /** * Fill codes of scopes. * * @param scopeNodeList node list with scopes definitions * @param scopes scopes * @param codes codes * @throws TextProcessorFactoryException any problem */ private void fillScopeCodes( NodeList scopeNodeList, Map<String, Scope> scopes, Map<String, Code> codes ) { for (int i = 0; i < scopeNodeList.getLength(); i++) { Element scopeElement = (Element) scopeNodeList.item(i); Scope scope = scopes.get(scopeElement.getAttribute(TAG_SCOPE_ATTR_NAME)); // Add codes to scope Set<Code> scopeCodes = new HashSet<Code>(); // bind exists codes NodeList coderefs = scopeElement.getElementsByTagNameNS(SCHEMA_LOCATION, TAG_CODEREF); for (int j = 0; j < coderefs.getLength(); j++) { Element ref = (Element) coderefs.item(j); String codeName = ref.getAttribute(TAG_CODEREF_ATTR_NAME); Code code = codes.get(codeName); if (code == null) { throw new TextProcessorFactoryException("Can't find code \"" + codeName + "\"."); } scopeCodes.add(code); } // Add inline codes NodeList inlineCodes = scopeElement.getElementsByTagNameNS(SCHEMA_LOCATION, TAG_CODE); for (int j = 0; j < inlineCodes.getLength(); j++) { // Inline element code Element ice = (Element) inlineCodes.item(j); scopeCodes.add(parseCode(ice, scopes)); } // Set codes to scope scope.setCodes(scopeCodes); } } /** * Parse scopes from XML * * @param scopeNodeList list with scopes definitions * @return scopes * @throws TextProcessorFactoryException any problems */ private Map<String, Scope> parseScopes(NodeList scopeNodeList) { Map<String, Scope> scopes = new HashMap<String, Scope>(); // Parse scopes for (int i = 0; i < scopeNodeList.getLength(); i++) { Element scopeElement = (Element) scopeNodeList.item(i); String name = scopeElement.getAttribute(TAG_SCOPE_ATTR_NAME); if (name.length() == 0) { throw new TextProcessorFactoryException("Illegal scope name. Scope name can't be empty."); } Scope scope = new Scope( name, nodeAttribute(scopeElement, TAG_SCOPE_ATTR_IGNORE_TEXT, Scope.DEFAULT_IGNORE_TEXT) ); scope.setStrong(nodeAttribute(scopeElement, TAG_SCOPE_ATTR_STRONG, Scope.DEFAULT_STRONG)); scopes.put(scope.getName(), scope); } // Set parents for (int i = 0; i < scopeNodeList.getLength(); i++) { Element scopeElement = (Element) scopeNodeList.item(i); String name = scopeElement.getAttribute(TAG_SCOPE_ATTR_NAME); Scope scope = scopes.get(name); if (scope == null) { throw new TextProcessorFactoryException( MessageFormat.format("Can't find scope \"{0}\".", name) ); } String parentName = nodeAttribute(scopeElement, TAG_SCOPE_ATTR_PARENT); if (parentName != null) { Scope parent = scopes.get(parentName); if (parent == null) { throw new TextProcessorFactoryException( MessageFormat.format("Can't find parent scope \"{0}\".", parentName) ); } scope.setParent(parent); } scope.setMax(nodeAttribute(scopeElement, TAG_SCOPE_ATTR_MAX, Scope.DEFAULT_MAX_VALUE)); scope.setMin(nodeAttribute(scopeElement, TAG_SCOPE_ATTR_MIN, Scope.DEFAULT_MIN_VALUE)); } return scopes; } /** * Parse codes from XML * * @param dc DOM document with configuration * @return codes * @throws TextProcessorFactoryException any problem */ private Map<String, Code> parseCodes(Document dc, Map<String, Scope> scopes) { Map<String, Code> codes = new HashMap<String, Code>(); NodeList codeNodeList = dc.getDocumentElement().getElementsByTagNameNS(SCHEMA_LOCATION, TAG_CODE); for (int i = 0; i < codeNodeList.getLength(); i++) { Code code = parseCode((Element) codeNodeList.item(i), scopes); codes.put(code.getName(), code); } return codes; } /** * Parse bb-code from DOM Node * * @param codeElement node, represent code wich * @return bb-code * @throws TextProcessorFactoryException if error format */ private Code parseCode(Element codeElement, Map<String, Scope> scopes) { // Code name Code code = new Code(nodeAttribute(codeElement, TAG_CODE_ATTR_NAME, Utils.generateRandomName())); // Code priority code.setPriority(nodeAttribute(codeElement, TAG_CODE_ATTR_PRIORITY, Code.DEFAULT_PRIORITY)); // Do show variables outside the code? code.setTransparent(nodeAttribute(codeElement, TAG_CODE_ATTR_TRANSPARENT, true)); // Template to building NodeList templateElements = codeElement.getElementsByTagNameNS(SCHEMA_LOCATION, TAG_TEMPLATE); if (templateElements.getLength() > 0) { code.setTemplate(parseTemplate(templateElements.item(0))); } else { throw new TextProcessorFactoryException("Illegal configuration. Can't find template of code."); } // Pattern to parsing NodeList patternElements = codeElement.getElementsByTagNameNS(SCHEMA_LOCATION, TAG_PATTERN); if (patternElements.getLength() > 0) { for (int i = 0; i < patternElements.getLength(); i++) { code.addPattern(parsePattern(patternElements.item(i), scopes)); } } else { throw new TextProcessorFactoryException("Illegal configuration. Can't find pattern of code."); } // return code return code; } /** * Parse code pattern for parse text. * * @param node pattern node with pattern description * @return list of pattern elements * @throws TextProcessorFactoryException If invalid pattern format */ private Pattern parsePattern(Node node, Map<String, Scope> scopes) { List<PatternElement> elements = new ArrayList<PatternElement>(); NodeList patternList = node.getChildNodes(); int patternLength = patternList.getLength(); if (patternLength <= 0) { throw new TextProcessorFactoryException("Invalid pattern. Pattern is empty."); } // Ignore case for all constants boolean ignoreCase = nodeAttribute(node, "ignoreCase", false); for (int k = 0; k < patternLength; k++) { Node el = patternList.item(k); short nodeType = el.getNodeType(); if (nodeType == Node.TEXT_NODE) { elements.add(new Constant(el.getNodeValue(), ignoreCase)); } else if (nodeType == Node.ELEMENT_NODE) { String tagName = el.getLocalName(); if (tagName.equals(TAG_CONSTANT)) { elements.add(parseConstant(el, ignoreCase)); } else if (tagName.equals(TAG_VAR)) { elements.add(parseNamedElement(el, scopes)); } else if (tagName.equals(TAG_JUNK)) { elements.add(new Junk()); } else if (tagName.equals(TAG_EOL)) { elements.add(parseEol(el)); } else if (tagName.equals(TAG_BOL)) { elements.add(new Bol()); } else if (tagName.equals(TAG_BLANKLINE)) { elements.add(new BlankLine(nodeAttribute(el, TAG_ATTR_GHOST, PatternElement.DEFAULT_GHOST_VALUE))); } else if (tagName.equals(TAG_URL)) { elements.add(parseUrl(el)); } else if (tagName.equals(TAG_EMAIL)){ elements.add(parseEmail(el)); } else { throw new TextProcessorFactoryException( MessageFormat.format("Invalid pattern. Unknown XML element [{0}].", tagName) ); } } else { throw new TextProcessorFactoryException("Invalid pattern. Unsupported XML node type."); } } return new Pattern(elements); } /** * Parse an URL tag. * * @param el tag element * @return Configuration URL. */ private Url parseUrl(Node el) { return new Url( nodeAttribute(el, TAG_VAR_ATTR_NAME, Url.DEFAULT_NAME), nodeAttribute(el, TAG_ATTR_GHOST, PatternElement.DEFAULT_GHOST_VALUE), nodeAttribute(el, TAG_URL_ATTR_LOCAL, Url.DEFAULT_LOCAL), nodeAttribute(el, TAG_URL_ATTR_SCHEMALESS, Url.DEFAULT_SCHEMALESS) ); } /** * Parse an email tag. * @param el tag element * @return Configuration EMAIL. */ private Email parseEmail(Node el){ return new Email( nodeAttribute(el, TAG_VAR_ATTR_NAME, Email.DEFAULT_NAME), nodeAttribute(el, TAG_ATTR_GHOST, PatternElement.DEFAULT_GHOST_VALUE) ); } private Eol parseEol(Node el) { return new Eol(nodeAttribute(el, TAG_ATTR_GHOST, PatternElement.DEFAULT_GHOST_VALUE)); } /** * Parse constant pattern element * * @param el DOM element * @param ignoreCase if true the constant must ignore case * @return constant definition */ private Constant parseConstant(Node el, boolean ignoreCase) { return new Constant( nodeAttribute(el, TAG_CONSTANT_ATTR_VALUE), nodeAttribute(el, TAG_CONSTANT_ATTR_IGNORE_CASE, ignoreCase), nodeAttribute(el, TAG_ATTR_GHOST, PatternElement.DEFAULT_GHOST_VALUE) ); } /** * Parse a pattern named element. Text or Variable. * * @param el a DOM-element * @param scopes map of scopes by name * @return Text or Variable */ private PatternElement parseNamedElement(Node el, Map<String, Scope> scopes) { PatternElement namedElement; if ( nodeAttribute(el, TAG_VAR_ATTR_PARSE, DEFAULT_PARSE_VALUE) && !nodeHasAttribute(el, TAG_VAR_ATTR_REGEX) && !nodeHasAttribute(el, TAG_VAR_ATTR_ACTION) ) { namedElement = parseText(el, scopes); } else { namedElement = parseVariable(el); } return namedElement; } /** * Parse text. Text is a part of pattern. * * @param el a DOM-element * @param scopes map of scopes by name * @return text */ private Text parseText(Node el, Map<String, Scope> scopes) { Text text; if (nodeAttribute(el, TAG_VAR_ATTR_INHERIT, DEFAULT_INHERIT_VALUE)) { text = new Text( nodeAttribute(el, TAG_VAR_ATTR_NAME, Variable.DEFAULT_NAME), null, nodeAttribute(el, TAG_VAR_ATTR_TRANSPARENT, false) ); } else { String scopeName = nodeAttribute(el, TAG_SCOPE, Scope.ROOT); Scope scope = scopes.get(scopeName); if (scope == null) { throw new TextProcessorFactoryException( MessageFormat.format("Scope \"{0}\" not found.", scopeName) ); } text = new Text( nodeAttribute(el, TAG_VAR_ATTR_NAME, Variable.DEFAULT_NAME), scope, nodeAttribute(el, TAG_VAR_ATTR_TRANSPARENT, false) ); } return text; } /** * Parse variable. The part of pattern. * * @param el DOM-element * @return variable */ private Variable parseVariable(Node el) { Variable variable; if (nodeHasAttribute(el, TAG_VAR_ATTR_REGEX)) { variable = new Variable( nodeAttribute(el, TAG_VAR_ATTR_NAME, Variable.DEFAULT_NAME), java.util.regex.Pattern.compile( nodeAttribute(el, TAG_VAR_ATTR_REGEX) ) ); } else { variable = new Variable(nodeAttribute(el, TAG_VAR_ATTR_NAME, Variable.DEFAULT_NAME)); } variable.setGhost(nodeAttribute(el, TAG_ATTR_GHOST, PatternElement.DEFAULT_GHOST_VALUE)); if (nodeHasAttribute(el, TAG_VAR_ATTR_ACTION)) { variable.setAction(Action.valueOf(nodeAttribute(el, TAG_VAR_ATTR_ACTION))); } return variable; } /** * Parse template for generate text. * * @param node template node * @return list of template elements */ private Template parseTemplate(Node node) { return new Template(parseTemplateElements(node)); } /** * Parse an IF expression. */ private If parseIf(Node node) { return new If( nodeAttribute(node, TAG_VAR_ATTR_NAME, Variable.DEFAULT_NAME), parseTemplateElements(node) ); } private List<TemplateElement> parseTemplateElements(Node node) { List<TemplateElement> elements = new ArrayList<TemplateElement>(); NodeList templateList = node.getChildNodes(); for (int k = 0; k < templateList.getLength(); k++) { Node el = templateList.item(k); if (el.getNodeType() == Node.ELEMENT_NODE) { String tagName = el.getLocalName(); if (tagName.equals(TAG_VAR)) { if (nodeHasAttribute(el, TAG_VAR_ATTR_FUNCTION)) { elements.add(new NamedValue( nodeAttribute(el, TAG_VAR_ATTR_NAME, Variable.DEFAULT_NAME), Function.valueOf(nodeAttribute(el, TAG_VAR_ATTR_FUNCTION)) )); } else { elements.add(new NamedValue(nodeAttribute(el, TAG_VAR_ATTR_NAME, Variable.DEFAULT_NAME))); } } else if (tagName.equals(TAG_IF)) { elements.add(parseIf(el)); } else { throw new TextProcessorFactoryException( MessageFormat.format("Invalid template. Unknown XML element [{0}].", tagName) ); } } else if (el.getNodeType() == Node.TEXT_NODE) { elements.add(new Constant(el.getNodeValue())); } else { throw new TextProcessorFactoryException("Invalid template. Unsupported XML node type."); } } return elements; } /** * Return node attribute value, if exists or default attribute value * * @param node XML-node * @param attributeName attributeName * @param defaultValue attribute default value * @return attribute value or default value */ private boolean nodeAttribute(Node node, String attributeName, boolean defaultValue) { boolean value = defaultValue; if (node.hasAttributes()) { Node attribute = node.getAttributes().getNamedItem(attributeName); if (attribute != null) { value = Boolean.valueOf(attribute.getNodeValue()); } } return value; } /** * Return node attribute value, if exists or default attibute value * * @param node XML-node * @param attributeName attributeName * @param defaultValue attribute default value * @return attribute value or default value */ private String nodeAttribute(Node node, String attributeName, String defaultValue) { String value = defaultValue; if (node.hasAttributes()) { Node attribute = node.getAttributes().getNamedItem(attributeName); if (attribute != null) { value = attribute.getNodeValue(); } } return value; } /** * Return node attribute value, if exists or null value * * @param node XML-node * @param attributeName attributeName * @return attribute value or default value */ private String nodeAttribute(Node node, String attributeName) { String value = null; if (node.hasAttributes()) { Node attribute = node.getAttributes().getNamedItem(attributeName); if (attribute != null) { value = attribute.getNodeValue(); } } return value; } /** * Return node attribute value, if exists or default attibute value * * @param node XML-node * @param attributeName attributeName * @param defaultValue attribute default value * @return attribute value or default value */ private int nodeAttribute(Node node, String attributeName, int defaultValue) { int value = defaultValue; if (node.hasAttributes()) { Node attribute = node.getAttributes().getNamedItem(attributeName); if (attribute != null) { value = Integer.decode(attribute.getNodeValue()); } } return value; } /** * Check node attribute. * * @param node XML-node * @param attributeName name of attribute * @return true if node has attribute with specified name * false if has not */ private boolean nodeHasAttribute(Node node, String attributeName) { return node.hasAttributes() && node.getAttributes().getNamedItem(attributeName) != null; } }