/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.model.experimental.schema; import org.waveprotocol.wave.model.document.operation.AnnotationBoundaryMap; import org.waveprotocol.wave.model.document.operation.Attributes; import org.waveprotocol.wave.model.document.operation.DocInitialization; import org.waveprotocol.wave.model.document.operation.DocInitializationCursor; import org.waveprotocol.wave.model.experimental.schema.IntermediateSchemaFragment.AttributesPatternBuilder; import org.waveprotocol.wave.model.experimental.schema.IntermediateSchemaFragment.CharacterPatternBuilder; import org.waveprotocol.wave.model.experimental.schema.IntermediateSchemaFragment.DirectPrologueFragment; import org.waveprotocol.wave.model.experimental.schema.IntermediateSchemaFragment.IntermediatePrologueEntry; import org.waveprotocol.wave.model.experimental.schema.IntermediateSchemaFragment.IntermediatePrologueFragment; import org.waveprotocol.wave.model.experimental.schema.IntermediateSchemaFragment.ReferencePrologueFragment; import org.waveprotocol.wave.model.util.Utf16Util; import org.waveprotocol.wave.model.util.Utf16Util.CodePointHandler; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; /** * A factory for schemas. * */ public final class SchemaFactory { /* * The following is a description of how createSchemaPattern() works. * * The given DocInitialization is traversed from start to finish in linear * order. During this process, pushHandler() is called whenever an element * start is encountered and popHandler() is called whenever an element end is * encountered. There is a handler class for each type of context that can * exist in a schema document. In this way, handlers are pushed onto and * popped off a stack in a way that reflects the nesting structure of the * elements in the schema document. * * When a handler is being popped off the stack, it may register the * information about whatever it has handled through a call to some * register*() method of the next element on the stack (which would be the * handler for its parent context). */ private static final class BoxedSchemaException extends RuntimeException { private final InvalidSchemaException exception; BoxedSchemaException(InvalidSchemaException e) { exception = e; } InvalidSchemaException unbox() { return exception; } } private abstract static class Handler { abstract Handler pushHandler(String type, Attributes attrs) throws InvalidSchemaException; abstract Handler popHandler() throws InvalidSchemaException; } /** * A handler for the contents of the root of the schema document. */ private static final class BaseHandler extends Handler { private static final String DEFINITION_TYPE_NAME = "definition"; private static final String ROOT_TYPE_NAME = "root"; private final Map<String, IntermediateSchemaFragment> definitions = new TreeMap<String, IntermediateSchemaFragment>(); private String root; @Override Handler pushHandler(String type, Attributes attrs) throws InvalidSchemaException { if (type.equals(DEFINITION_TYPE_NAME)) { String name = extractName(DEFINITION_TYPE_NAME, attrs); return new DefinitionHandler(name, this); } else if (type.equals(ROOT_TYPE_NAME)) { if (root != null) { throw new InvalidSchemaException("More than one declaration of root"); } root = extractName(ROOT_TYPE_NAME, attrs); return new EmptyHandler(this); } throw new InvalidSchemaException("Unrecognized element type: " + type); } @Override Handler popHandler() { return null; } void registerDefinition(String name, IntermediateSchemaFragment pattern) throws InvalidSchemaException { if (definitions.containsKey(name)) { throw new InvalidSchemaException("Duplicate definition for: " + name); } definitions.put(name, pattern); } } /** * A handler for any pattern description that can directly contain a * collection of "element" subpatterns. */ private abstract static class ElementCollectionHandler extends Handler { abstract void registerElement(String elementType, IntermediateSchemaFragment pattern); } /** * A handler for a pattern description for a definition or element. */ private abstract static class PatternHandler extends ElementCollectionHandler { private static final String PROLOGUE_TYPE_NAME = "prologue"; private static final String ATTRIBUTE_TYPE_NAME = "attribute"; private static final String ELEMENT_TYPE_NAME = "element"; private static final String TEXT_TYPE_NAME = "text"; private static final String REFERENCE_TYPE_NAME = "reference"; private static final String REQUIRED_ATTRIBUTE_NAME = "required"; private static final String VALUES_ATTRIBUTE_NAME = "values"; private static final String TRUE_ATTRIBUTE_VALUE = "true"; private static final String FALSE_ATTRIBUTE_VALUE = "false"; private static final String TYPE_ATTRIBUTE_NAME = "type"; private static final String CHARACTERS_ATTRIBUTE_NAME = "characters"; private static final String BLACKLIST_ATTRIBUTE_VALUE = "blacklist"; private static final String WHITELIST_ATTRIBUTE_VALUE = "whitelist"; private final AttributesPatternBuilder attributesPattern = new AttributesPatternBuilder(); private final List<IntermediatePrologueFragment> prologueFragments = new ArrayList<IntermediatePrologueFragment>(); // NOTE: This could be optimized with a trie. private final Map<String, IntermediateSchemaFragment> freeElements = new TreeMap<String, IntermediateSchemaFragment>(); private final Set<String> references = new TreeSet<String>(); private final CharacterPatternBuilder characterPattern = new CharacterPatternBuilder(); @Override final Handler pushHandler(String type, Attributes attrs) throws InvalidSchemaException { // NOTE: Is it worth using a map here? if (type.equals(PROLOGUE_TYPE_NAME)) { return new PrologueHandler(this); } else if (type.equals(ATTRIBUTE_TYPE_NAME)) { handleAttribute(attrs); return new EmptyHandler(this); } else if (type.equals(ELEMENT_TYPE_NAME)) { String name = extractName(ELEMENT_TYPE_NAME, attrs); if (freeElements.containsKey(name)) { throw new InvalidSchemaException("Element pattern defined more than once: " + name); } return new ElementHandler(name, this); } else if (type.equals(TEXT_TYPE_NAME)) { handleText(attrs); return new EmptyHandler(this); } else if (type.equals(REFERENCE_TYPE_NAME)) { handleReference(attrs); return new EmptyHandler(this); } throw new InvalidSchemaException("Unrecognized element type: " + type); } @Override final void registerElement(String elementType, IntermediateSchemaFragment pattern) { freeElements.put(elementType, pattern); } final void registerPrologue(List<IntermediatePrologueEntry> prologueEntries) { this.prologueFragments.add(new DirectPrologueFragment(prologueEntries)); } final IntermediateSchemaFragment extractPattern() { return new IntermediateSchemaFragment(attributesPattern, prologueFragments, freeElements, references, characterPattern); } final void handleAttribute(Attributes attrs) throws InvalidSchemaException { String name = attrs.get(NAME_ATTRIBUTE_NAME); String values = attrs.get(VALUES_ATTRIBUTE_NAME); String required = attrs.get(REQUIRED_ATTRIBUTE_NAME); if (name == null) { throw new InvalidSchemaException("Missing attribute: " + NAME_ATTRIBUTE_NAME); } if (values == null) { throw new InvalidSchemaException("Missing attribute: " + VALUES_ATTRIBUTE_NAME); } if (required == null) { throw new InvalidSchemaException("Missing attribute: " + REQUIRED_ATTRIBUTE_NAME); } if (attrs.size() != 3) { throw new InvalidSchemaException("Encountered more attributes than the expected three"); } checkAttributeName(name); if (required.equals(TRUE_ATTRIBUTE_VALUE)) { attributesPattern.addRequired(name); } else if (!required.equals(FALSE_ATTRIBUTE_VALUE)) { throw new InvalidSchemaException( "Invalid value for attribute " + REQUIRED_ATTRIBUTE_NAME + ": " + required); } RegularExpressionChecker.checkRegularExpression(values); attributesPattern.addValuePattern(name, values); } final void handleText(Attributes attrs) throws InvalidSchemaException { String type = attrs.get(TYPE_ATTRIBUTE_NAME); String chars = attrs.get(CHARACTERS_ATTRIBUTE_NAME); if (type == null) { throw new InvalidSchemaException("Missing attribute: " + TYPE_ATTRIBUTE_NAME); } if (chars == null) { throw new InvalidSchemaException("Missing attribute: " + CHARACTERS_ATTRIBUTE_NAME); } if (attrs.size() != 2) { throw new InvalidSchemaException("Encountered more attributes than the expected two"); } if (type.equals(BLACKLIST_ATTRIBUTE_VALUE)) { characterPattern.blacklistCharacters(extractCodePoints(chars)); } else if (type.equals(WHITELIST_ATTRIBUTE_VALUE)) { characterPattern.whitelistCharacters(extractCodePoints(chars)); } else { throw new InvalidSchemaException( "Invalid value for attribute " + TYPE_ATTRIBUTE_NAME + ": " + type); } } final void handleReference(Attributes attrs) throws InvalidSchemaException { String name = extractName(REFERENCE_TYPE_NAME, attrs); prologueFragments.add(new ReferencePrologueFragment(name)); references.add(name); } } /** * A handler for a pattern description for a definition. */ private static final class DefinitionHandler extends PatternHandler { private final String name; private final BaseHandler previousTop; DefinitionHandler(String name, BaseHandler previousTop) { this.name = name; this.previousTop = previousTop; } @Override Handler popHandler() throws InvalidSchemaException { previousTop.registerDefinition(name, extractPattern()); return previousTop; } } /** * A handler for a pattern description for an element. */ private static final class ElementHandler extends PatternHandler { private final String elementType; private final ElementCollectionHandler previousTop; ElementHandler(String elementType, ElementCollectionHandler previousTop) throws InvalidSchemaException { this.elementType = elementType; this.previousTop = previousTop; checkElementType(elementType); } @Override Handler popHandler() { previousTop.registerElement(elementType, extractPattern()); return previousTop; } } /** * A handler for a pattern description for a prologue. */ private static final class PrologueHandler extends ElementCollectionHandler { private static final String ELEMENT_TYPE_NAME = "element"; private final PatternHandler previousTop; private final List<IntermediatePrologueEntry> prologueEntries = new ArrayList<IntermediatePrologueEntry>(); PrologueHandler(PatternHandler previousTop) { this.previousTop = previousTop; } @Override final Handler pushHandler(String type, Attributes attrs) throws InvalidSchemaException { if (type.equals(ELEMENT_TYPE_NAME)) { String name = extractName(ELEMENT_TYPE_NAME, attrs); return new ElementHandler(name, this); } throw new InvalidSchemaException( "Element encountered where no child element is allowed: " + type); } @Override Handler popHandler() { previousTop.registerPrologue(prologueEntries); return previousTop; } @Override void registerElement(String elementType, IntermediateSchemaFragment pattern) { prologueEntries.add(new IntermediatePrologueEntry(elementType, pattern)); } } /** * A handler for any context that should have no child contexts. */ private static final class EmptyHandler extends Handler { private final Handler previousTop; EmptyHandler(Handler previousTop) { this.previousTop = previousTop; } @Override final Handler pushHandler(String type, Attributes attrs) throws InvalidSchemaException { throw new InvalidSchemaException("Unrecognized element type: " + type); } @Override Handler popHandler() { return previousTop; } } /** * A builder that builds up a <code>SchemaPattern</code> when a * <code>DocInitialization</code> representing the schema pattern is applied * to it. */ private static final class SchemaBuilder implements DocInitializationCursor { private final BaseHandler baseHandler = new BaseHandler(); private Handler handler = baseHandler; @Override public void characters(String chars) { throw new BoxedSchemaException( new InvalidSchemaException("Encountered character data in schema: " + chars)); } @Override public void elementStart(String type, Attributes attrs) { try { handler = handler.pushHandler(type, attrs); } catch (InvalidSchemaException e) { throw new BoxedSchemaException(e); } } @Override public void elementEnd() { try { handler = handler.popHandler(); } catch (InvalidSchemaException e) { throw new BoxedSchemaException(e); } } @Override public void annotationBoundary(AnnotationBoundaryMap map) { throw new BoxedSchemaException( new InvalidSchemaException("Encountered annotation boundary")); } public SchemaPattern buildSchema() throws InvalidSchemaException { if (handler != baseHandler) { // This should normally not occur with proper usage of this class. throw new InvalidSchemaException("Ill-formed schema"); } if (baseHandler.root == null) { throw new InvalidSchemaException("Root not specified"); } return IntermediateSchemaFragment.compile(baseHandler.definitions, baseHandler.root); } } /** * A code point extractor. */ private static final CodePointHandler<List<Integer>> codePointExtractor = new CodePointHandler<List<Integer>>() { List<Integer> codePoints = new ArrayList<Integer>(); @Override public List<Integer> codePoint(int cp) { codePoints.add(cp); return null; } @Override public List<Integer> endOfString() { return codePoints; } @Override public List<Integer> unpairedSurrogate(char c) { return ERROR_LIST; } }; /** * A dummy list used as an error indicator used by <code>codePointExtractor</code>. */ private static final List<Integer> ERROR_LIST = new ArrayList<Integer>() {}; private static final String NAME_ATTRIBUTE_NAME = "name"; private SchemaFactory() {} private static String extractName(String elementType, Attributes attrs) throws InvalidSchemaException { String name = attrs.get(NAME_ATTRIBUTE_NAME); if (name == null) { throw new InvalidSchemaException( "Missing attribute \"" + NAME_ATTRIBUTE_NAME + "\" in element: " + elementType); } if (attrs.size() != 1) { throw new InvalidSchemaException( "Encountered an attribute other than \"" + NAME_ATTRIBUTE_NAME + "\" in element: " + elementType); } return name; } /** * Extracts code points from a given character string. * * @param chars a character string from which to extract code points * @return the code points extracted from the given string */ private static List<Integer> extractCodePoints(String chars) throws InvalidSchemaException { List<Integer> codePoints = Utf16Util.traverseUtf16String(chars, codePointExtractor); if (codePoints == ERROR_LIST) { throw new InvalidSchemaException("Invalid code point in string: " + chars); } return codePoints; } private static void checkElementType(String name) throws InvalidSchemaException { if (!Utf16Util.isXmlName(name)) { throw new InvalidSchemaException("Invalid element type: " + name); } } private static void checkAttributeName(String name) throws InvalidSchemaException { if (!Utf16Util.isXmlName(name)) { throw new InvalidSchemaException("Invalid attribute name: " + name); } } /** * Creates a <code>SchemaPattern</code> from its representation as a * <code>DocInitialization</code>. * * @param schemaDescription the schema represented as a * <code>DocInitialization</code> * @return the constructed <code>SchemaPattern</code> * @throws InvalidSchemaException if the given <code>DocInitialization</code> * does not represent a valid schema */ public static SchemaPattern createSchemaPattern(DocInitialization schemaDescription) throws InvalidSchemaException { SchemaBuilder builder = new SchemaBuilder(); try { schemaDescription.apply(builder); } catch (BoxedSchemaException e) { throw e.unbox(); } return builder.buildSchema(); } }