/** * 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.experimental.schema.SchemaPattern.Prologue; import org.waveprotocol.wave.model.experimental.schema.SchemaPattern.PrologueEntry; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; /** * An intermediate representation for the process of constructing a * <code>SchemaPattern</code> from a schema document. * */ final class IntermediateSchemaFragment { /* * A note on how a schema pattern is represented and how the compilation to a * schema pattern works: * * A schema pattern is represented as a graph of references between different * pattern objects, with each object representing some subpattern of the full * pattern. Each schema pattern contains information about its prologue, * non-prologue elements, allowed attributes, and allowed character data. The * prologue's pattern is stored as a list, where each item in the list * corresponds to an element in the prologue and contains a reference to a * schema pattern for that element. The information corresponding to * non-prologue child elements is stored as a map from the element type to the * schema pattern object that corresponds to that element type. * * Compilation is split into two stages. * * In the first stage, only a partial compile (of just the top-level * structure) of each definition is performed. The compilation of the patterns * corresponding to both prologue and non-prologue child elements of the * definition is deferred (but their pattern objects are created and remain * uninitialized at this stage). This creates object references to schema * pattern objects required by a later full compile and also needed by the * partial top-level compiles of other definitions. Any circular references * encountered in this first stage will indicate a bad infinite recursion, and * should trigger an <code>InvalidSchemaException</code>. * * In the second stage of the compilation, all deferred compilation is brought * to completion. Any circular references encountered in this second stage are * the result of safe recursion. */ /** * An intermediate format for a prologue entry. */ static final class IntermediatePrologueEntry { private final String type; private final IntermediateSchemaFragment fragment; IntermediatePrologueEntry(String type, IntermediateSchemaFragment fragment) { this.type = type; this.fragment = fragment; } String elementType() { return type; } IntermediateSchemaFragment fragment() { return fragment; } } /** * An intermediate format for a fragment of a prologue, containing a sequence * of adjacent prologue entries. */ abstract static class IntermediatePrologueFragment { /** * Performs a partial compile of the top-level of the prologue fragment, * creating a list of prologue entries in the fragment which pairs element * types with their uninitialized schema patterns (whose full compilation * has been deferred). * * @param state the state used by the compiler * @return the list of prologue entries, where the compilation of each * entry's schema pattern has been deferred */ abstract List<PrologueEntry> resolveTopLevel(CompilerState state) throws InvalidSchemaException; /** * Performs a full compile of the prologue fragment, returning a list of * prologue entries representing the content of the prologue fragment. * * @param state the state used by the compiler * @return the list of prologue entries */ abstract List<PrologueEntry> compile(CompilerState state) throws InvalidSchemaException; } /** * A prologue fragment arising from the direct specification of the fragment's * content (as opposed to through referencing a definition). */ static final class DirectPrologueFragment extends IntermediatePrologueFragment { private final List<IntermediatePrologueEntry> prologueEntries; DirectPrologueFragment(List<IntermediatePrologueEntry> prologueEntries) { this.prologueEntries = prologueEntries; } @Override List<PrologueEntry> resolveTopLevel(CompilerState state) { List<PrologueEntry> resolved = new ArrayList<PrologueEntry>(); for (IntermediatePrologueEntry entry : prologueEntries) { SchemaPattern pattern = new SchemaPattern(); IntermediateSchemaFragment fragment = entry.fragment(); resolved.add(new PrologueEntry(entry.elementType(), pattern)); state.deferredCompiles.add(new DeferredCompile(fragment, pattern)); } return resolved; } @Override List<PrologueEntry> compile(CompilerState state) throws InvalidSchemaException { List<PrologueEntry> compiled = new ArrayList<PrologueEntry>(); for (IntermediatePrologueEntry entry : prologueEntries) { compiled.add(new PrologueEntry(entry.type, entry.fragment().compile(state))); } return compiled; } } /** * A prologue fragment arising from referencing a definition. */ static final class ReferencePrologueFragment extends IntermediatePrologueFragment { private final String reference; ReferencePrologueFragment(String reference) { this.reference = reference; } @Override List<PrologueEntry> resolveTopLevel(CompilerState state) throws InvalidSchemaException { IntermediateSchemaFragment fragment = state.lookupDefinition(reference); return fragment.resolveTopLevel(reference, state).pattern.prologue(); } @Override List<PrologueEntry> compile(CompilerState state) { return state.references.get(reference).pattern.prologue(); } } /** * Accumulates attribute patterns, which can then be used to construct a * <code>AttributesValidator</code>. */ static final class AttributesPatternBuilder { // NOTE: This could be optimized with a trie. private final Set<String> requiredAttributes = new TreeSet<String>(); // NOTE: This could be optimized with a trie. private final Map<String, String> attributeValuePatterns = new TreeMap<String, String>(); void addRequired(String attributeName) { requiredAttributes.add(attributeName); } void addValuePattern(String attributeName, String values) throws InvalidSchemaException { if (attributeValuePatterns.containsKey(attributeName)) { throw new InvalidSchemaException( "Attribute pattern defined more than once: " + attributeName); } attributeValuePatterns.put(attributeName, values); } void importFrom(AttributesPatternBuilder other) throws InvalidSchemaException { requiredAttributes.addAll(other.requiredAttributes); for (Map.Entry<String, String> entry : other.attributeValuePatterns.entrySet()) { String key = entry.getKey(); if (attributeValuePatterns.containsKey(key)) { throw new InvalidSchemaException("Attribute pattern defined more than once: " + key); } attributeValuePatterns.put(key, entry.getValue()); } } AttributesValidator createValidator() { // NOTE: This could be optimized with a trie. Map<String, ValueValidator> attributeValidators = new TreeMap<String, ValueValidator>(); for (Map.Entry<String, String> entry : attributeValuePatterns.entrySet()) { String name = entry.getKey(); String values = entry.getValue(); attributeValidators.put(name, ValueValidator.fromRegex(values)); } return new AttributesValidator(requiredAttributes, attributeValidators); } } /** * Accumulates characters, which can then be used to construct a <code>CharacterValidator</code>. */ static final class CharacterPatternBuilder { private Set<Integer> characters = new TreeSet<Integer>(); private boolean blacklistCharacters = false; void whitelistCharacters(Collection<Integer> chars) { if (blacklistCharacters) { characters.removeAll(chars); } else { characters.addAll(chars); } } void blacklistCharacters(Collection<Integer> chars) { Set<Integer> oldCharacters = characters; characters = new TreeSet<Integer>(); if (blacklistCharacters) { for (int character : chars) { if (oldCharacters.contains(character)) { characters.add(character); } } } else { for (int character : chars) { if (!oldCharacters.contains(character)) { characters.add(character); } } blacklistCharacters = true; } } void importFrom(CharacterPatternBuilder other) { if (other.blacklistCharacters) { blacklistCharacters(other.characters); } else { whitelistCharacters(other.characters); } } CharacterValidator createValidator() { return blacklistCharacters ? CharacterValidator.disallowedCharacters(characters) : CharacterValidator.allowedCharacters(characters); } } /** * A container for the information needed to perform the deferred compilation * of <code>IntermediateSchemaFragment</code>. */ private static final class DeferredCompile { private final IntermediateSchemaFragment source; private final SchemaPattern target; DeferredCompile(IntermediateSchemaFragment source, SchemaPattern target) { this.source = source; this.target = target; } } /** * The result of resolving a reference. */ private static final class ReferenceData { final SchemaPattern pattern; final Map<String, SchemaPattern> potentialChildren; final AttributesPatternBuilder attributesPattern; final CharacterPatternBuilder characterPattern; ReferenceData(SchemaPattern pattern, Map<String, SchemaPattern> potentialChildren, AttributesPatternBuilder attributesPattern, CharacterPatternBuilder characterPattern) { this.pattern = pattern; this.potentialChildren = potentialChildren; this.attributesPattern = attributesPattern; this.characterPattern = characterPattern; } } /** * The general storage container for state needed by the compiler to function. */ private static final class CompilerState { final Map<String, IntermediateSchemaFragment> definitions; final Map<String, ReferenceData> references = new TreeMap<String, ReferenceData>(); final Set<String> undergoingResolution = new TreeSet<String>(); final List<DeferredCompile> deferredCompiles = new ArrayList<DeferredCompile>(); CompilerState(Map<String, IntermediateSchemaFragment> definitions) { this.definitions = definitions; } IntermediateSchemaFragment lookupDefinition(String name) throws InvalidSchemaException { IntermediateSchemaFragment result = definitions.get(name); if (result == null) { throw new InvalidSchemaException("Reference to undefined pattern definition: " + name); } return result; } } private final AttributesPatternBuilder attributesPattern; private final List<IntermediatePrologueFragment> prologue; private final Map<String, IntermediateSchemaFragment> allowedChildren; private final Set<String> references; private final CharacterPatternBuilder characterPattern; IntermediateSchemaFragment( AttributesPatternBuilder attributesPattern, List<IntermediatePrologueFragment> prologue, Map<String, IntermediateSchemaFragment> allowedChildren, Set<String> references, CharacterPatternBuilder characterPattern) { this.attributesPattern = attributesPattern; this.prologue = prologue; this.allowedChildren = allowedChildren; this.references = references; this.characterPattern = characterPattern; } private void importReferences(Map<String, SchemaPattern> allowedChildrenMap, CompilerState state) throws InvalidSchemaException { for (String ref : references) { ReferenceData refData = state.lookupDefinition(ref).resolveTopLevel(ref, state); characterPattern.importFrom(refData.characterPattern); attributesPattern.importFrom(refData.attributesPattern); for (Map.Entry<String, SchemaPattern> entry : refData.potentialChildren.entrySet()) { String key = entry.getKey(); if (allowedChildrenMap.containsKey(key)) { throw new InvalidSchemaException( "Subpattern defined more than once for element type: " + key); } allowedChildrenMap.put(key, entry.getValue()); } } } /** * Performs a partial compile of just the top level of a referenced definition * (which is represented by this object). * * @param reference the name of the definition which this object represents * @param state state needed by the compiler * @return the result of the partial compilation */ private ReferenceData resolveTopLevel(String reference, CompilerState state) throws InvalidSchemaException { if (state.undergoingResolution.contains(reference)) { throw new InvalidSchemaException("Infinite recursion encountered"); } ReferenceData referenceData = state.references.get(reference); if (referenceData != null) { return referenceData; } state.undergoingResolution.add(reference); /* * NOTE: Is there a nicer way than mutating attributesPattern (and the other * fields) that is as efficient? */ List<PrologueEntry> prologueEntries = new ArrayList<PrologueEntry>(); Map<String, SchemaPattern> allowedChildrenMap = new TreeMap<String, SchemaPattern>(); for (Map.Entry<String, IntermediateSchemaFragment> entry : allowedChildren.entrySet()) { String key = entry.getKey(); SchemaPattern pattern = new SchemaPattern(); allowedChildrenMap.put(key, pattern); state.deferredCompiles.add(new DeferredCompile(entry.getValue(), pattern)); } importReferences(allowedChildrenMap, state); for (IntermediatePrologueFragment prologueFragment : prologue) { // TODO(user): Concatenating prologues can by slightly optimized here. prologueEntries.addAll(prologueFragment.resolveTopLevel(state)); } SchemaPattern compiledPattern = new SchemaPattern(); compiledPattern.initialize(attributesPattern.createValidator(), new Prologue(prologueEntries), allowedChildrenMap, characterPattern.createValidator()); state.undergoingResolution.remove(reference); referenceData = new ReferenceData(compiledPattern, allowedChildrenMap, attributesPattern, characterPattern); state.references.put(reference, referenceData); return referenceData; } /** * Compiles this intermediate fragment into the given uninitialized pattern object. * * @param pattern an uninitialized pattern object into which to compile this intermediate fragment * @param state state needed by the compiler */ private void compileTo(SchemaPattern pattern, CompilerState state) throws InvalidSchemaException { Map<String, SchemaPattern> allowedChildrenMap = new TreeMap<String, SchemaPattern>(); for (Map.Entry<String, IntermediateSchemaFragment> entry : allowedChildren.entrySet()) { allowedChildrenMap.put(entry.getKey(), entry.getValue().compile(state)); } importReferences(allowedChildrenMap, state); List<PrologueEntry> compiledPrologue = new ArrayList<PrologueEntry>(); for (IntermediatePrologueFragment prologueFragment : prologue) { compiledPrologue.addAll(prologueFragment.compile(state)); } pattern.initialize(attributesPattern.createValidator(), new Prologue(compiledPrologue), allowedChildrenMap, characterPattern.createValidator()); } /** * Compiles this intermediate fragment into a new pattern object. * * @param state state needed by the compiler * @return a new pattern object */ private SchemaPattern compile(CompilerState state) throws InvalidSchemaException { SchemaPattern pattern = new SchemaPattern(); compileTo(pattern, state); return pattern; } /** * Compiles a map associating <code>IntermediateSchemaFragment</code>s with * definition names into a <code>SchemaPattern</code>. * * @param definitions a map from definition names to the intermediate * representation of the pattern defined by the corresponding * definition * @param root the root pattern * @return the schema pattern */ static SchemaPattern compile(Map<String, IntermediateSchemaFragment> definitions, String root) throws InvalidSchemaException { CompilerState state = new CompilerState(definitions); for (Map.Entry<String, IntermediateSchemaFragment> definition : definitions.entrySet()) { definition.getValue().resolveTopLevel(definition.getKey(), state); } for (DeferredCompile deferredCompile : state.deferredCompiles) { deferredCompile.source.compileTo(deferredCompile.target, state); } ReferenceData rootReference = state.references.get(root); if (rootReference == null) { throw new InvalidSchemaException("Root not defined"); } return rootReference.pattern; } }