Index: src/test/java/org/apache/jackrabbit/spi/commons/nodetype/compact/TestAll.java =================================================================== --- src/test/java/org/apache/jackrabbit/spi/commons/nodetype/compact/TestAll.java (revision 0) +++ src/test/java/org/apache/jackrabbit/spi/commons/nodetype/compact/TestAll.java (revision 0) @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Test suite that includes all test cases for compact node type tools. + */ +public class TestAll extends TestCase { + + /** + * Returns a Test suite that executes all tests inside this + * package. + * + * @return a Test suite that executes all tests inside this + * package. + */ + public static Test suite() { + TestSuite suite = new TestSuite("Compact Node Type Tools Tests"); + suite.addTestSuite(CompactNodeTypeDefTest.class); + return suite; + } +} \ No newline at end of file Index: src/test/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefTest.java =================================================================== --- src/test/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefTest.java (revision 0) +++ src/test/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefTest.java (revision 0) @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + + +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.List; + +import junit.framework.TestCase; + +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping; +import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeDefDiff; +import org.apache.jackrabbit.value.ValueFactoryImpl; + +public class CompactNodeTypeDefTest extends TestCase { + private static final String TEST_FILE = "cnd-reader-test-input.cnd"; + + public void testCompactNodeTypeDef() throws Exception { + + // Read in node type def from test file + Reader reader = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(TEST_FILE)); + CompactNodeTypeDefReader cndReader = new CompactNodeTypeDefReader(reader, TEST_FILE, + new QNodeTypeDefinitionsBuilderImpl()); + + List ntdList1 = cndReader.getNodeTypeDefs(); + NamespaceMapping nsm = cndReader.getNamespaceMapping(); + NamePathResolver resolver = new DefaultNamePathResolver(nsm); + + // Put imported node type def back into CND form with CND writer + StringWriter sw = new StringWriter(); + CompactNodeTypeDefWriter.write(ntdList1, nsm, resolver, ValueFactoryImpl.getInstance(), sw); + + // Rerun the reader on the product of the writer + cndReader = new CompactNodeTypeDefReader(new StringReader(sw.toString()), TEST_FILE, + new QNodeTypeDefinitionsBuilderImpl()); + + List ntdList2 = cndReader.getNodeTypeDefs(); + + if (ntdList1.size() == 0 || ntdList1.size() != ntdList2.size()) + fail("Exported node type definition was not successfully read back in"); + else + for(int k = 0; k < ntdList1.size(); k++) { + QNodeTypeDefinition ntd1 = (QNodeTypeDefinition) ntdList1.get(k); + QNodeTypeDefinition ntd2 = (QNodeTypeDefinition) ntdList2.get(k); + + NodeTypeDefDiff diff = NodeTypeDefDiff.create(ntd1, ntd2); + if (diff.isModified() && !diff.isTrivial()){ + fail("Exported node type definition was not successfully read back in. " + + ntd2.getName() + "differs from original"); + } + } + } +} Index: src/test/resources/cnd-reader-test-input.cnd =================================================================== --- src/test/resources/cnd-reader-test-input.cnd (revision 0) +++ src/test/resources/cnd-reader-test-input.cnd (revision 0) @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ + + + + + + +//------------------------------------------------------------------------------ +// E X A M P L E T Y P E S +//------------------------------------------------------------------------------ + +[ex:NodeType] > ex:ParentNodeType1, ex:ParentNodeType2 + orderable mixin + - ex:property (long) = '1', '2' primary mandatory autocreated protected multiple version < '[1,10]' + + ex:node (ex:RequiredNodeType1, ex:RequiredNodeType2) = ex:RequiredNodeType1 mandatory autocreated protected multiple version + +[ex:AnotherNodeType] > ex:NodeType + - * (string) = 'a residual property' multiple + + * (ex:RequiredNodeType1) multiple + +[ex:Empty] + +[ex:Reference] + - ex:ref (reference) mandatory protected < 'ex:ref' + +[ex:Name] + - ex:name (name) < 'ex:name' + +[ex:Path] + - ex:path (path) < 'ex:a/ex:b' + +//------------------------------------------------------------------------------ +// B A S E T Y P E S +//------------------------------------------------------------------------------ + +[nt:base] + - jcr:primaryType (name) mandatory autocreated protected compute + - jcr:mixinTypes (name) protected multiple compute + +[nt:unstructured] + orderable + - * (undefined) multiple + - * (undefined) + + * (nt:base) = nt:unstructured multiple version + +[mix:referenceable] + mixin + - jcr:uuid (string) mandatory autocreated protected initialize + +[mix:lockable] + mixin + - jcr:lockOwner (string) protected ignore + - jcr:lockIsDeep (boolean) protected ignore + +//------------------------------------------------------------------------------ +// V E R S I O N I N G +//------------------------------------------------------------------------------ + +[mix:versionable] > mix:referenceable + mixin + - jcr:versionHistory (reference) mandatory protected + < 'nt:versionHistory' + - jcr:baseVersion (reference) mandatory protected ignore + < 'nt:version' + - jcr:isCheckedOut (boolean) = 'true' mandatory autocreated protected ignore + - jcr:predecessors (reference) mandatory protected multiple + < 'nt:version' + - jcr:mergeFailed (reference) protected multiple abort + +[nt:versionHistory] > mix:referenceable + - jcr:versionableUuid (string) mandatory autocreated protected abort + + jcr:rootVersion (nt:version) = nt:version mandatory autocreated protected abort + + jcr:versionLabels (nt:versionLabels) = nt:versionLabels mandatory autocreated protected abort + + * (nt:version) = nt:version protected abort + +[nt:versionLabels] + - * (reference) protected abort + < 'nt:version' + +[nt:version] > mix:referenceable + - jcr:created (date) mandatory autocreated protected abort + - jcr:predecessors (reference) protected multiple abort + < 'nt:version' + - jcr:successors (reference) protected multiple abort + < 'nt:version' + + jcr:frozenNode (nt:frozenNode) protected abort + +[nt:frozenNode] > mix:referenceable + orderable + - jcr:frozenPrimaryType (name) mandatory autocreated protected abort + - jcr:frozenMixinTypes (name) protected multiple abort + - jcr:frozenUuid (string) mandatory autocreated protected abort + - * (undefined) protected abort + - * (undefined) protected multiple abort + + * (nt:base) protected multiple abort + +[nt:versionedChild] + - jcr:childVersionHistory (reference) mandatory autocreated protected abort + < 'nt:versionHistory' + +//------------------------------------------------------------------------------ +// N O D E T Y P E S +//------------------------------------------------------------------------------ + +[nt:nodeType] + - jcr:nodeTypeName (name) mandatory + - jcr:supertypes (name) multiple + - jcr:isMixin (boolean) mandatory + - jcr:hasOrderableChildNodes (boolean) mandatory + - jcr:primaryItemName (name) + + jcr:propertyDefinition (nt:propertyDefinition) = nt:propertyDefinition multiple version + + jcr:childNodeDefinition (nt:childNodeDefinition) = nt:childNodeDefinition multiple version + +[nt:propertyDefinition] + - jcr:name (name) + - jcr:autoCreated (boolean) mandatory + - jcr:mandatory (boolean) mandatory + - jcr:onParentVersion (string) mandatory + < 'COPY', 'VERSION', 'INITIALIZE', 'COMPUTE', 'IGNORE', 'ABORT' + - jcr:protected (boolean) mandatory + - jcr:requiredType (string) mandatory + < 'STRING', 'BINARY', 'LONG', 'DOUBLE', 'BOOLEAN', 'DATE', 'NAME', 'PATH', 'REFERENCE', 'UNDEFINED' + - jcr:valueConstraints (string) multiple + - jcr:defaultValues (undefined) multiple + - jcr:multiple (boolean) mandatory + +[nt:childNodeDefinition] + - jcr:name (name) + - jcr:autoCreated (boolean) mandatory + - jcr:mandatory (boolean) mandatory + - jcr:onParentVersion (string) mandatory + < 'COPY', 'VERSION', 'INITIALIZE', 'COMPUTE', 'IGNORE', 'ABORT' + - jcr:protected (boolean) mandatory + - jcr:requiredPrimaryTypes (name) = 'nt:base' mandatory multiple + - jcr:defaultPrimaryType (name) + - jcr:sameNameSiblings (boolean) mandatory + +//------------------------------------------------------------------------------ +// M I S C +//------------------------------------------------------------------------------ + +[nt:hierarchyNode] + - jcr:created (date) autocreated protected initialize + +[nt:folder] > nt:hierarchyNode + + * (nt:hierarchyNode) version + +[nt:file] > nt:hierarchyNode + + jcr:content (nt:base) primary mandatory + +[nt:linkedFile] > nt:hierarchyNode + - jcr:content (reference) primary mandatory + +[nt:resource] > mix:referenceable + - jcr:encoding (string) + - jcr:mimeType (string) mandatory + - jcr:data (binary) primary mandatory + - jcr:lastModified (date) mandatory ignore + +[nt:query] + - jcr:statement (string) + - jcr:language (string) + +//------------------------------------------------------------------------------ +// J A C K R A B B I T I N T E R N A L S +//------------------------------------------------------------------------------ + +[rep:nodeTypes] + + * (nt:nodeType) = nt:nodeType protected abort + +[rep:root] > nt:unstructured + orderable + + jcr:system (rep:system) = rep:system mandatory ignore + +[rep:system] + orderable + + jcr:versionStorage (rep:versionStorage) = rep:versionStorage mandatory protected abort + + jcr:nodeTypes (rep:nodeTypes) = rep:nodeTypes mandatory protected abort + + * (nt:base) = nt:unstructured multiple ignore + +[rep:versionStorage] + + * (nt:versionHistory) = nt:versionHistory protected multiple abort + + * (rep:versionStorage) = rep:versionStorage protected multiple abort + \ No newline at end of file Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefDiff.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefDiff.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/NodeTypeDefDiff.java (revision 0) @@ -0,0 +1,621 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.PropertyType; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; + +/** + * A NodeTypeDefDiff represents the result of the comparison of + * two node type definitions. + *

+ * The result of the comparison can be categorized as one of the following types: + *

+ * NONE inidcates that there is no modification at all. + *

+ * A TRIVIAL modification has no impact on the consistency + * of existing content and does not affect existing/assigned definition id's. + * The following modifications are considered TRIVIAL: + *

    + *
  • changing node type orderableChildNodes flag + *
  • changing node type primaryItemName value + *
  • adding non-mandatory property/child node + *
  • changing property/child node protected flag + *
  • changing property/child node onParentVersion value + *
  • changing property/child node mandatory flag to false + *
  • changing property/child node autoCreated flag + *
  • changing child node defaultPrimaryType + *
  • changing child node sameNameSiblings flag to true + *
  • weaken property valueConstraints (e.g. by removing completely + * or by adding to existing or by making a single constraint less restrictive) + *
  • changing property defaultValues + *
+ *

+ * A MINOR modification has no impact on the consistency + * of existing content but does affect existing/assigned definition id's. + * The following modifications are considered MINOR: + *

    + *
  • changing specific property/child node name to * + *
  • weaken child node requiredPrimaryTypes (e.g. by removing) + *
  • changing specific property requiredType to undefined + *
  • changing property multiple flag to true + *
+ *

+ * A MAJOR modification affects the consistency of + * existing content and does change existing/assigned definition id's. + * All modifications that are neither TRIVIAL nor + * MINOR are considered MAJOR. + * + * @see #getType() + */ +public class NodeTypeDefDiff { + + /** + * no modification + */ + public static final int NONE = 0; + /** + * trivial modification: does neither affect consistency of existing content + * nor does it change existing/assigned definition id's + */ + public static final int TRIVIAL = 1; + /** + * minor modification: does not affect consistency of existing content but + * does change existing/assigned definition id's + */ + public static final int MINOR = 2; + /** + * major modification: does affect consistency of existing content + * and does change existing/assigned definition id's + */ + public static final int MAJOR = 3; + + private final QNodeTypeDefinition oldDef; + private final QNodeTypeDefinition newDef; + private int type; + + private final List propDefDiffs = new ArrayList(); + private final List childNodeDefDiffs = new ArrayList(); + + /** + * Constructor + */ + private NodeTypeDefDiff(QNodeTypeDefinition oldDef, QNodeTypeDefinition newDef) { + this.oldDef = oldDef; + this.newDef = newDef; + init(); + } + + /** + * + */ + private void init() { + if (oldDef.equals(newDef)) { + // definitions are identical + type = NONE; + } else { + // definitions are not identical, determine type of modification + + // assume TRIVIAL change by default + type = TRIVIAL; + + // check supertypes + int tmpType = supertypesDiff(); + if (tmpType > type) { + type = tmpType; + } + + // check mixin flag (MAJOR modification) + tmpType = mixinFlagDiff(); + if (tmpType > type) { + type = tmpType; + } + + // no need to check orderableChildNodes flag (TRIVIAL modification) + + // check property definitions + tmpType = buildPropDefDiffs(); + if (tmpType > type) { + type = tmpType; + } + + // check child node definitions + tmpType = buildChildNodeDefDiffs(); + if (tmpType > type) { + type = tmpType; + } + } + } + + /** + * @param oldDef + * @param newDef + * @return + */ + public static NodeTypeDefDiff create(QNodeTypeDefinition oldDef, QNodeTypeDefinition newDef) { + if (oldDef == null || newDef == null) { + throw new IllegalArgumentException("arguments can not be null"); + } + if (!oldDef.getName().equals(newDef.getName())) { + throw new IllegalArgumentException("at least node type names must be matching"); + } + return new NodeTypeDefDiff(oldDef, newDef); + } + + /** + * @return + */ + public boolean isModified() { + return type != NONE; + } + + /** + * @return + */ + public boolean isTrivial() { + return type == TRIVIAL; + } + + /** + * @return + */ + public boolean isMinor() { + return type == MINOR; + } + + /** + * @return + */ + public boolean isMajor() { + return type == MAJOR; + } + + /** + * Returns the type of modification as expressed by the following constants: + *

    + *
  • NONE: no modification at all + *
  • TRIVIAL: does neither affect consistency of + * existing content nor does it change existing/assigned definition id's + *
  • MINOR: does not affect consistency of existing + * content but does change existing/assigned definition id's + *
  • MAJOR: does affect consistency of existing + * content and does change existing/assigned definition id's + *
+ * + * @return the type of modification + */ + public int getType() { + return type; + } + + /** + * @return + */ + public int mixinFlagDiff() { + return oldDef.isMixin() != newDef.isMixin() ? MAJOR : NONE; + } + + /** + * @return + */ + public int supertypesDiff() { + return !Arrays.equals(oldDef.getSupertypes(), newDef.getSupertypes()) ? MAJOR : NONE; + } + + /** + * @return + */ + private int buildPropDefDiffs() { + /** + * propDefId determinants: declaringNodeType, name, requiredType, multiple + * todo: try also to match entries with modified id's + */ + + int maxType = NONE; + QPropertyDefinition[] pda1 = oldDef.getPropertyDefs(); + HashMap defs1 = new HashMap(); + for (int i = 0; i < pda1.length; i++) { + defs1.put(pda1[i].getName(), pda1[i]); + } + + QPropertyDefinition[] pda2 = newDef.getPropertyDefs(); + HashMap defs2 = new HashMap(); + for (int i = 0; i < pda2.length; i++) { + defs2.put(pda2[i].getName(), pda2[i]); + } + + /** + * walk through defs1 and process all entries found in + * both defs1 & defs2 and those found only in defs1 + */ + Iterator iter = defs1.keySet().iterator(); + while (iter.hasNext()) { + Name name = (Name) iter.next(); + QPropertyDefinition def1 = (QPropertyDefinition) defs1.get(name); + QPropertyDefinition def2 = (QPropertyDefinition) defs2.get(name); + PropDefDiff diff = new PropDefDiff(def1, def2); + if (diff.getType() > maxType) { + maxType = diff.getType(); + } + propDefDiffs.add(diff); + defs2.remove(name); + } + + /** + * defs2 by now only contains entries found in defs2 only; + * walk through defs2 and process all remaining entries + */ + iter = defs2.keySet().iterator(); + while (iter.hasNext()) { + Name name = (Name) iter.next(); + QPropertyDefinition def = (QPropertyDefinition) defs2.get(name); + PropDefDiff diff = new PropDefDiff(null, def); + if (diff.getType() > maxType) { + maxType = diff.getType(); + } + propDefDiffs.add(diff); + } + + return maxType; + } + + /** + * @return + */ + private int buildChildNodeDefDiffs() { + /** + * nodeDefId determinants: declaringNodeType, name, requiredPrimaryTypes + * todo: try also to match entries with modified id's + */ + + int maxType = NONE; + QNodeDefinition[] cnda1 = oldDef.getChildNodeDefs(); + HashMap defs1 = new HashMap(); + for (int i = 0; i < cnda1.length; i++) { + defs1.put(cnda1[i].getName(), cnda1[i]); + } + + QNodeDefinition[] cnda2 = newDef.getChildNodeDefs(); + HashMap defs2 = new HashMap(); + for (int i = 0; i < cnda2.length; i++) { + defs2.put(cnda2[i].getName(), cnda2[i]); + } + + /** + * walk through defs1 and process all entries found in + * both defs1 & defs2 and those found only in defs1 + */ + Iterator iter = defs1.keySet().iterator(); + while (iter.hasNext()) { + Name name = (Name) iter.next(); + QNodeDefinition def1 = (QNodeDefinition) defs1.get(name); + QNodeDefinition def2 = (QNodeDefinition) defs2.get(name); + ChildNodeDefDiff diff = new ChildNodeDefDiff(def1, def2); + if (diff.getType() > maxType) { + maxType = diff.getType(); + } + childNodeDefDiffs.add(diff); + defs2.remove(name); + } + + /** + * defs2 by now only contains entries found in defs2 only; + * walk through defs2 and process all remaining entries + */ + iter = defs2.keySet().iterator(); + while (iter.hasNext()) { + Name name = (Name) iter.next(); + QNodeDefinition def = (QNodeDefinition) defs2.get(name); + ChildNodeDefDiff diff = new ChildNodeDefDiff(null, def); + if (diff.getType() > maxType) { + maxType = diff.getType(); + } + childNodeDefDiffs.add(diff); + } + + return maxType; + } + + public String toString() { + String result = getClass().getName() + "[\n\tnodeTypeName=" + + oldDef.getName(); + + result += ",\n\tmixinFlagDiff=" + modificationTypeToString(mixinFlagDiff()); + result += ",\n\tsupertypesDiff=" + modificationTypeToString(supertypesDiff()); + + result += ",\n\tpropertyDifferences=[\n"; + result += toString(propDefDiffs); + result += "\t]"; + + result += ",\n\tchildNodeDifferences=[\n"; + result += toString(childNodeDefDiffs); + result += "\t]\n"; + result += "]\n"; + + return result; + } + + private String toString(List childItemDefDiffs) { + String result = ""; + for (Iterator iter = childItemDefDiffs.iterator(); iter.hasNext();) { + ChildItemDefDiff propDefDiff = (ChildItemDefDiff) iter.next(); + result += "\t\t" + propDefDiff; + if (iter.hasNext()) { + result += ","; + } + result += "\n"; + } + return result; + } + + private String modificationTypeToString(int modifcationType) { + String typeString = "unknown"; + switch (modifcationType) { + case NONE: + typeString = "NONE"; + break; + case TRIVIAL: + typeString = "TRIVIAL"; + break; + case MINOR: + typeString = "MINOR"; + break; + case MAJOR: + typeString = "MAJOR"; + break; + } + return typeString; + } + + + //--------------------------------------------------------< inner classes > + + abstract class ChildItemDefDiff { + protected final QItemDefinition oldDef; + protected final QItemDefinition newDef; + protected int type; + + ChildItemDefDiff(QItemDefinition oldDef, QItemDefinition newDef) { + this.oldDef = oldDef; + this.newDef = newDef; + init(); + } + + protected void init() { + // determine type of modification + if (isAdded()) { + if (!newDef.isMandatory()) { + // adding a non-mandatory child item is a TRIVIAL change + type = TRIVIAL; + } else { + // adding a mandatory child item is a MAJOR change + type = MAJOR; + } + } else if (isRemoved()) { + // removing a child item is a MAJOR change + type = MAJOR; + } else { + /** + * neither added nor removed => has to be either identical + * or modified + */ + if (oldDef.equals(newDef)) { + // identical + type = NONE; + } else { + // modified + if (oldDef.isMandatory() != newDef.isMandatory() + && newDef.isMandatory()) { + // making a child item mandatory is a MAJOR change + type = MAJOR; + } else { + if (!oldDef.definesResidual() + && newDef.definesResidual()) { + // just making a child item residual is a MINOR change + type = MINOR; + } else { + if (!oldDef.getName().equals(newDef.getName())) { + // changing the name of a child item is a MAJOR change + type = MAJOR; + } else { + // all other changes are TRIVIAL + type = TRIVIAL; + } + } + } + } + } + } + + public int getType() { + return type; + } + + public boolean isAdded() { + return oldDef == null && newDef != null; + } + + public boolean isRemoved() { + return oldDef != null && newDef == null; + } + + public boolean isModified() { + return oldDef != null && newDef != null + && !oldDef.equals(newDef); + } + + public String toString() { + String typeString = modificationTypeToString(getType()); + + String operationString; + if (isAdded()) { + operationString = "ADDED"; + } else if (isModified()) { + operationString = "MODIFIED"; + } else if (isRemoved()) { + operationString = "REMOVED"; + } else { + operationString = "NONE"; + } + + QItemDefinition itemDefinition = (oldDef != null) ? oldDef : newDef; + + return getClass().getName() + "[itemName=" + + itemDefinition.getName() + ", type=" + typeString + + ", operation=" + operationString + "]"; + } + + } + + public class PropDefDiff extends ChildItemDefDiff { + + PropDefDiff(QPropertyDefinition oldDef, QPropertyDefinition newDef) { + super(oldDef, newDef); + } + + public QPropertyDefinition getOldDef() { + return (QPropertyDefinition) oldDef; + } + + public QPropertyDefinition getNewDef() { + return (QPropertyDefinition) newDef; + } + + protected void init() { + super.init(); + /** + * only need to do comparison if base class implementation + * detected a non-MAJOR modification (i.e. TRIVIAL or MINOR); + * no need to check for additions or removals as this is already + * handled in base class implementation. + */ + if (isModified() && type != NONE && type != MAJOR) { + /** + * check if valueConstraints were made more restrictive + * (constraints are ORed) + */ + String[] vca1 = getOldDef().getValueConstraints(); + HashSet set1 = new HashSet(); + for (int i = 0; i < vca1.length; i++) { + set1.add(vca1[i]); + } + String[] vca2 = getNewDef().getValueConstraints(); + HashSet set2 = new HashSet(); + for (int i = 0; i < vca2.length; i++) { + set2.add(vca2[i]); + } + + if (set1.isEmpty() && !set2.isEmpty()) { + // added constraint where there was no constraint (MAJOR change) + type = MAJOR; + } else if (!set2.containsAll(set1) && !set2.isEmpty()) { + // removed existing constraint (MAJOR change) + type = MAJOR; + } + + // no need to check defaultValues (TRIVIAL change) + + if (type == TRIVIAL) { + int t1 = getOldDef().getRequiredType(); + int t2 = getNewDef().getRequiredType(); + if (t1 != t2) { + if (t2 == PropertyType.UNDEFINED) { + // changed getRequiredType to UNDEFINED (MINOR change) + type = MINOR; + } else { + // changed getRequiredType to specific type (MAJOR change) + type = MAJOR; + } + } + boolean b1 = getOldDef().isMultiple(); + boolean b2 = getNewDef().isMultiple(); + if (b1 != b2) { + if (b2) { + // changed multiple flag to true (MINOR change) + type = MINOR; + } else { + // changed multiple flag to false (MAJOR change) + type = MAJOR; + } + } + } + } + } + } + + public class ChildNodeDefDiff extends ChildItemDefDiff { + + ChildNodeDefDiff(QNodeDefinition oldDef, QNodeDefinition newDef) { + super(oldDef, newDef); + } + + public QNodeDefinition getOldDef() { + return (QNodeDefinition) oldDef; + } + + public QNodeDefinition getNewDef() { + return (QNodeDefinition) newDef; + } + + protected void init() { + super.init(); + /** + * only need to do comparison if base class implementation + * detected a non-MAJOR modification (i.e. TRIVIAL or MINOR); + * no need to check for additions or removals as this is already + * handled in base class implementation. + */ + if (isModified() && type != NONE && type != MAJOR) { + + boolean b1 = getOldDef().allowsSameNameSiblings(); + boolean b2 = getNewDef().allowsSameNameSiblings(); + if (b1 != b2 && !b2) { + // changed sameNameSiblings flag to false (MAJOR change) + type = MAJOR; + } + + // no need to check defaultPrimaryType (TRIVIAL change) + + if (type == TRIVIAL) { + List l1 = Arrays.asList(getOldDef().getRequiredPrimaryTypes()); + List l2 = Arrays.asList(getNewDef().getRequiredPrimaryTypes()); + if (!l1.equals(l2)) { + if (l1.containsAll(l2)) { + // removed requiredPrimaryType (MINOR change) + type = MINOR; + } else { + // added requiredPrimaryType (MAJOR change) + type = MAJOR; + } + } + } + } + } + } +} Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/ValueConstraint.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/ValueConstraint.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/ValueConstraint.java (revision 0) @@ -0,0 +1,911 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype; + +import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.PathResolver; +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.Path; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.value.DateValue; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; +import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.slf4j.LoggerFactory; +import org.slf4j.Logger; + +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.NamespaceException; +import javax.jcr.nodetype.ConstraintViolationException; +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * ValueConstraint and its subclasses are used to check the + * syntax of a value constraint and to test if a specific value satisfies + * it. + */ +public abstract class ValueConstraint { + + protected static Logger log = LoggerFactory.getLogger(ValueConstraint.class); + + public static final ValueConstraint[] EMPTY_ARRAY = new ValueConstraint[0]; + + private final String qualifiedDefinition; + + protected ValueConstraint(String qualifiedDefinition) { + this.qualifiedDefinition = qualifiedDefinition; + } + + /** + * For constraints that are not namespace prefix mapping sensitive this + * method returns the same result as {@link #getQualifiedDefinition()}. + *

+ * Those that are namespace prefix mapping sensitive (e.g. + * NameConstraint, PathConstraint and + * ReferenceConstraint) use the given nsResolver + * to reflect the current mapping in the returned value. + * In other words: subclasses, that need to make a conversion to JCR value + * must overwrite this and return a value that has all qualified names + * and path elements resolved. + * + * @return the definition of this constraint. + * @see #getQualifiedDefinition() + * @param resolver + */ + public String getDefinition(NamePathResolver resolver) { + return qualifiedDefinition; + } + + /** + * By default the qualified definition is the same as the JCR definition. + * + * @return the qualified definition String + * @see #getDefinition(NamePathResolver) + */ + public String getQualifiedDefinition() { + return qualifiedDefinition; + } + + /** + * + * @param value + * @throws ConstraintViolationException + * @throws RepositoryException + */ + abstract void check(QValue value) throws ConstraintViolationException, RepositoryException; + + + //-----------------------------------------< java.lang.Object overrides >--- + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ValueConstraint) { + return qualifiedDefinition.equals(((ValueConstraint) other).qualifiedDefinition); + } else { + return false; + } + } + + /** + * Returns the hashCode of the definition String + * + * @return the hashCode of the definition String + * @see Object#hashCode() + */ + public int hashCode() { + return qualifiedDefinition.hashCode(); + } + + //-----------------------------------< static factory and check methods >--- + /** + * Create a new ValueConstraint from the String representation. + * Note, that the definition must be in the qualified format in case the type + * indicates {@link PropertyType#NAME}, {@link PropertyType#PATH} or {@link PropertyType#REFERENCE} + * + * @param type + * @param qualifiedDefinition + * @return + * @throws InvalidConstraintException + */ + public static ValueConstraint create(int type, String qualifiedDefinition) + throws InvalidConstraintException { + if (qualifiedDefinition == null) { + throw new IllegalArgumentException("illegal definition (null)"); + } + switch (type) { + // constraints which are not qName senstive + case PropertyType.STRING: + return new StringConstraint(qualifiedDefinition); + + case PropertyType.BOOLEAN: + return new BooleanConstraint(qualifiedDefinition); + + case PropertyType.BINARY: + return new NumericConstraint(qualifiedDefinition); + + case PropertyType.DATE: + return new DateConstraint(qualifiedDefinition); + + case PropertyType.LONG: + case PropertyType.DOUBLE: + return new NumericConstraint(qualifiedDefinition); + + // qName sensitive constraints: create from qualified string + case PropertyType.NAME: + return new NameConstraint(qualifiedDefinition); + + case PropertyType.PATH: + return new PathConstraint(qualifiedDefinition); + + case PropertyType.REFERENCE: + return new ReferenceConstraint(qualifiedDefinition); + + default: + throw new IllegalArgumentException("unknown/unsupported target type for constraint: " + + PropertyType.nameFromValue(type)); + } + } + + /** + * + * @param type + * @param definition + * @param resolver + * @return + * @throws InvalidConstraintException + */ + public static ValueConstraint create(int type, String definition, + NamePathResolver resolver) + throws InvalidConstraintException { + if (definition == null) { + throw new IllegalArgumentException("Illegal definition (null) for ValueConstraint."); + } + switch (type) { + case PropertyType.STRING: + return new StringConstraint(definition); + + case PropertyType.BOOLEAN: + return new BooleanConstraint(definition); + + case PropertyType.BINARY: + return new NumericConstraint(definition); + + case PropertyType.DATE: + return new DateConstraint(definition); + + case PropertyType.LONG: + case PropertyType.DOUBLE: + return new NumericConstraint(definition); + + case PropertyType.NAME: + return new NameConstraint(definition, resolver); + + case PropertyType.PATH: + return new PathConstraint(definition, resolver); + + case PropertyType.REFERENCE: + return new ReferenceConstraint(definition, resolver); + + default: + throw new IllegalArgumentException("Unknown/unsupported target type for constraint: " + PropertyType.nameFromValue(type)); + } + } + + /** + * Tests if the value constraints defined in the property definition + * pd are satisfied by the the specified values. + *

+ * Note that the protected flag is not checked. Also note that no + * type conversions are attempted if the type of the given values does not + * match the required type as specified in the given definition. + * + * @param pd + * @param values + * @throws ConstraintViolationException + */ + public static void checkValueConstraints(QPropertyDefinition pd, QValue[] values) + throws ConstraintViolationException, RepositoryException { + // check multi-value flag + if (!pd.isMultiple() && values != null && values.length > 1) { + throw new ConstraintViolationException("the property is not multi-valued"); + } + + String[] constraints = pd.getValueConstraints(); + if (constraints == null || constraints.length == 0) { + // no constraints to check + return; + } + if (values != null && values.length > 0) { + // check value constraints on every value + for (int i = 0; i < values.length; i++) { + // constraints are OR-ed together + boolean satisfied = false; + ConstraintViolationException cve = null; + for (int j = 0; j < constraints.length && !satisfied; j++) { + try { + ValueConstraint cnstr = ValueConstraint.create(pd.getRequiredType(), constraints[j]); + cnstr.check(values[i]); + satisfied = true; + } catch (ConstraintViolationException e) { + cve = e; + } catch (InvalidConstraintException e) { + cve = new ConstraintViolationException(e.getMessage(), e); + } + } + if (!satisfied) { + // re-throw last exception we encountered + throw cve; + } + } + } + } +} + +//---------------------------------------------< Subclass BooleanConstraint >--- +/** + * BooleanConstraint ... + */ +class BooleanConstraint extends ValueConstraint { + final boolean reqBool; + + BooleanConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // constraint format: 'true' or 'false' + if (definition.equals("true")) { + reqBool = true; + } else if (definition.equals("false")) { + reqBool = false; + } else { + String msg = "'" + definition + + "' is not a valid value constraint format for BOOLEAN values"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + + /** + * @see ValueConstraint#check(QValue) + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + switch (value.getType()) { + case PropertyType.BOOLEAN: + boolean b = Boolean.valueOf(value.getString()).booleanValue(); + if (b != reqBool) { + throw new ConstraintViolationException("'" + b + "' does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + return; + + default: + String msg = "BOOLEAN constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} + +//----------------------------------------------< Subclass StringConstraint >--- +/** + * StringConstraint ... + */ +class StringConstraint extends ValueConstraint { + final Pattern pattern; + + StringConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // constraint format: regexp + try { + pattern = Pattern.compile(definition); + } catch (PatternSyntaxException pse) { + String msg = "'" + definition + "' is not valid regular expression syntax"; + log.debug(msg); + throw new InvalidConstraintException(msg, pse); + } + } + + /** + * @see ValueConstraint#check(QValue) + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + switch (value.getType()) { + case PropertyType.STRING: + String text = value.getString(); + Matcher matcher = pattern.matcher(text); + if (!matcher.matches()) { + throw new ConstraintViolationException("'" + text + "' does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + return; + + default: + String msg = "STRING constraint can not be applied to value of type: " + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} + +//---------------------------------------------< Subclass NumericConstraint >--- +/** + * NumericConstraint ... + */ +class NumericConstraint extends ValueConstraint { + final boolean lowerInclusive; + final Double lowerLimit; + final boolean upperInclusive; + final Double upperLimit; + + NumericConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // format: '(, )', '[, ]', '(, )' etc. + Pattern pattern = Pattern.compile("([\\(\\[]) *(\\-?\\d+\\.?\\d*)? *, *(\\-?\\d+\\.?\\d*)? *([\\)\\]])"); + Matcher matcher = pattern.matcher(definition); + if (matcher.matches()) { + try { + // group 1 is lower inclusive/exclusive + String s = matcher.group(1); + lowerInclusive = s.equals("["); + // group 2 is lower limit + s = matcher.group(2); + if (s == null || s.length() == 0) { + lowerLimit = null; + } else { + lowerLimit = Double.valueOf(matcher.group(2)); + } + // group 3 is upper limit + s = matcher.group(3); + if (s == null || s.length() == 0) { + upperLimit = null; + } else { + upperLimit = Double.valueOf(matcher.group(3)); + } + // group 4 is lower inclusive/exclusive + s = matcher.group(4); + upperInclusive = s.equals("]"); + if (lowerLimit == null && upperLimit == null) { + String msg = "'" + definition + "' is not a valid value constraint" + + " format for numeric types: neither lower- nor upper-limit specified"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + if (lowerLimit != null && upperLimit != null) { + if (lowerLimit.doubleValue() > upperLimit.doubleValue()) { + String msg = "'" + definition + + "' is not a valid value constraint format for numeric types: lower-limit exceeds upper-limit"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + } catch (NumberFormatException nfe) { + String msg = "'" + definition + + "' is not a valid value constraint format for numeric types"; + log.debug(msg); + throw new InvalidConstraintException(msg, nfe); + } + } else { + String msg = "'" + definition + + "' is not a valid value constraint format for numeric values"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + + private void check(double number) throws ConstraintViolationException { + if (lowerLimit != null) { + if (lowerInclusive) { + if (number < lowerLimit.doubleValue()) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } else { + if (number <= lowerLimit.doubleValue()) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } + } + if (upperLimit != null) { + if (upperInclusive) { + if (number > upperLimit.doubleValue()) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } else { + if (number >= upperLimit.doubleValue()) { + throw new ConstraintViolationException(number + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } + } + } + + /** + * @see ValueConstraint#check(QValue) + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + switch (value.getType()) { + case PropertyType.LONG: + check(value.getLong()); + return; + + case PropertyType.DOUBLE: + check(value.getDouble()); + return; + + case PropertyType.BINARY: + long length = value.getLength(); + if (length != -1) { + check(length); + } else { + log.warn("failed to determine length of binary value"); + } + return; + + default: + String msg = "numeric constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} + +//------------------------------------------------< Subclass DateConstraint >--- +/** + * DateConstraint ... + */ +class DateConstraint extends ValueConstraint { + final boolean lowerInclusive; + final Calendar lowerLimit; + final boolean upperInclusive; + final Calendar upperLimit; + + DateConstraint(String definition) throws InvalidConstraintException { + super(definition); + + // format: '(, )', '[, ]', '[, ]' etc. + Pattern pattern = Pattern.compile("([\\(\\[]) *([0-9TZ\\.\\+-:]*)? *, *([0-9TZ\\.\\+-:]*)? *([\\)\\]])"); + Matcher matcher = pattern.matcher(definition); + if (matcher.matches()) { + try { + // group 1 is lower inclusive/exclusive + String s = matcher.group(1); + lowerInclusive = s.equals("["); + // group 2 is lower limit + s = matcher.group(2); + if (s == null || s.length() == 0) { + lowerLimit = null; + } else { + lowerLimit = DateValue.valueOf(matcher.group(2)).getDate(); + } + // group 3 is upper limit + s = matcher.group(3); + if (s == null || s.length() == 0) { + upperLimit = null; + } else { + upperLimit = DateValue.valueOf(matcher.group(3)).getDate(); + } + // group 4 is upepr inclusive/exclusive + s = matcher.group(4); + upperInclusive = s.equals("]"); + + if (lowerLimit == null && upperLimit == null) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates: neither min- nor max-date specified"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + if (lowerLimit != null && upperLimit != null) { + if (lowerLimit.after(upperLimit)) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates: min-date > max-date"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + } catch (ValueFormatException vfe) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates"; + log.debug(msg); + throw new InvalidConstraintException(msg, vfe); + } catch (RepositoryException re) { + String msg = "'" + definition + + "' is not a valid value constraint format for dates"; + log.debug(msg); + throw new InvalidConstraintException(msg, re); + } + } else { + String msg = "'" + definition + + "' is not a valid value constraint format for dates"; + log.debug(msg); + throw new InvalidConstraintException(msg); + } + } + + private void check(Calendar cal) throws ConstraintViolationException { + if (cal == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + if (lowerLimit != null) { + if (lowerInclusive) { + if (cal.getTimeInMillis() < lowerLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } else { + if (cal.getTimeInMillis() <= lowerLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } + } + if (upperLimit != null) { + if (upperInclusive) { + if (cal.getTimeInMillis() > upperLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } else { + if (cal.getTimeInMillis() >= upperLimit.getTimeInMillis()) { + throw new ConstraintViolationException(cal + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } + } + } + + /** + * @see ValueConstraint#check(QValue) + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + switch (value.getType()) { + case PropertyType.DATE: + check(value.getCalendar()); + return; + + default: + String msg = "DATE constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} + +//------------------------------------------------< Subclass PathConstraint >--- +/** + * PathConstraint ... + */ +class PathConstraint extends ValueConstraint { + + final Path path; + final boolean deep; + + PathConstraint(String qualifiedDefinition) { + super(qualifiedDefinition); + // constraint format: qualified absolute or relative path with optional trailing wildcard + deep = qualifiedDefinition.endsWith("*"); + // TODO improve. don't rely on a specific factory impl + path = PathFactoryImpl.getInstance().create(qualifiedDefinition); + } + + PathConstraint(String definition, PathResolver resolver) + throws InvalidConstraintException { + super(definition); + + // constraint format: absolute or relative path with optional trailing wildcard + deep = definition.endsWith("*"); + if (deep) { + // trim trailing wildcard before building path + definition = definition.substring(0, definition.length() - 1); + } + try { + path = resolver.getQPath(definition); + } catch (NameException e) { + String msg = "Invalid path expression specified as value constraint: " + definition; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } catch (NamespaceException e) { + String msg = "Invalid path expression specified as value constraint: " + definition; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } + } + + /** + * Uses {@link NamePathResolver#getJCRPath(Path)} to convert the + * qualified Path into a JCR path. + * + * @see ValueConstraint#getDefinition(NamePathResolver) + * @param resolver + */ + public String getDefinition(NamePathResolver resolver) { + try { + String p = resolver.getJCRPath(path); + if (!deep) { + return p; + } else if (path.denotesRoot()) { + return p + "*"; + } else { + return p + "/*"; + } + } catch (NamespaceException e) { + // should never get here, return raw definition as fallback + return getQualifiedDefinition(); + } + } + + /** + * Returns the String representation of the path. + * + * @return String representation of the path. + * @see ValueConstraint#getQualifiedDefinition() + */ + public String getQualifiedDefinition() { + return path.toString(); + } + + /** + * @see ValueConstraint#check(QValue) + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + switch (value.getType()) { + case PropertyType.PATH: + Path p = value.getPath(); + // normalize paths before comparing them + Path p0, p1; + try { + p0 = path.getNormalizedPath(); + p1 = p.getNormalizedPath(); + } catch (RepositoryException e) { + throw new ConstraintViolationException("path not valid: " + e); + } + if (deep) { + try { + if (!p0.isAncestorOf(p1)) { + throw new ConstraintViolationException(p + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } catch (RepositoryException e) { + // can't compare relative with absolute path + throw new ConstraintViolationException(p + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } else { + // exact match required + if (!p0.equals(p1)) { + throw new ConstraintViolationException(p + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + } + return; + + default: + String msg = "PATH constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} + +//------------------------------------------------< Subclass NameConstraint >--- +/** + * NameConstraint ... + */ +class NameConstraint extends ValueConstraint { + + private final Name name; + + NameConstraint(String qualifiedDefinition) { + super(qualifiedDefinition); + // constraint format: String representation of qualified name + // TODO improve. don't rely on a specific factory impl + name = NameFactoryImpl.getInstance().create(qualifiedDefinition); + } + + NameConstraint(String definition, NameResolver resolver) + throws InvalidConstraintException { + super(definition); + // constraint format: JCR name in prefix form + try { + name = resolver.getQName(definition); + } catch (NameException e) { + String msg = "invalid name specified as value constraint: " + + definition; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } catch (NamespaceException e) { + String msg = "invalid name specified as value constraint: " + + definition; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } + } + + /** + * Uses {@link NamePathResolver#getJCRName(Name)} to convert the + * qualified Name into a JCR name. + * + * @see ValueConstraint#getDefinition(NamePathResolver) + * @param resolver + */ + public String getDefinition(NamePathResolver resolver) { + try { + return resolver.getJCRName(name); + } catch (NamespaceException e) { + // should never get here, return raw definition as fallback + return getQualifiedDefinition(); + } + } + + /** + * Returns the String representation of the qualified name + * + * @return String representation of the qualified name + * @see ValueConstraint#getQualifiedDefinition() + */ + public String getQualifiedDefinition() { + return name.toString(); + } + + /** + * @see ValueConstraint#check(QValue) + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("null value does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + switch (value.getType()) { + case PropertyType.NAME: + Name n = value.getName(); + if (!name.equals(n)) { + throw new ConstraintViolationException(n + + " does not satisfy the constraint '" + + getQualifiedDefinition() + "'"); + } + return; + + default: + String msg = "NAME constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} + +//-------------------------------------------< Subclass ReferenceConstraint >--- +/** + * ReferenceConstraint ... + */ +class ReferenceConstraint extends ValueConstraint { + + private final Name ntName; + + ReferenceConstraint(String qualifiedDefinition) { + super(qualifiedDefinition); + // format: qualified node type name + // TODO improve. don't rely on a specific factory impl + ntName = NameFactoryImpl.getInstance().create(qualifiedDefinition); + } + + ReferenceConstraint(String definition, NamePathResolver resolver) throws InvalidConstraintException { + super(definition); + + // format: node type name + try { + ntName = resolver.getQName(definition); + } catch (IllegalNameException ine) { + String msg = "invalid node type name specified as value constraint: " + + definition; + log.debug(msg); + throw new InvalidConstraintException(msg, ine); + } catch (NamespaceException e) { + String msg = "invalid node type name specified as value constraint: " + + definition; + log.debug(msg); + throw new InvalidConstraintException(msg, e); + } + } + + /** + * Uses {@link NamePathResolver#getJCRName(Name)} to convert the + * qualified nodetype name into a JCR name. + * + * @see ValueConstraint#getDefinition(NamePathResolver) + * @param resolver + */ + public String getDefinition(NamePathResolver resolver) { + try { + return resolver.getJCRName(ntName); + } catch (NamespaceException npde) { + // should never get here, return raw definition as fallback + return getQualifiedDefinition(); + } + } + + /** + * Returns the String representation of the qualified nodetype name. + * + * @return String representation of the qualified nodetype name. + */ + public String getQualifiedDefinition() { + return ntName.toString(); + } + + /** + * @see ValueConstraint#check(QValue) + */ + void check(QValue value) throws ConstraintViolationException, RepositoryException { + if (value == null) { + throw new ConstraintViolationException("Null value does not satisfy the constraint '" + getQualifiedDefinition() + "'"); + } + switch (value.getType()) { + case PropertyType.REFERENCE: + // TODO check REFERENCE value constraint (requires a session) + log.warn("validation of REFERENCE constraint is not yet implemented"); + return; + + default: + String msg = "REFERENCE constraint can not be applied to value of type: " + + PropertyType.nameFromValue(value.getType()); + log.debug(msg); + throw new RepositoryException(msg); + } + } +} + + Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/Lexer.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/Lexer.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/Lexer.java (revision 0) @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + +import java.io.StreamTokenizer; +import java.io.Reader; +import java.io.IOException; + +/** + * Lexer + */ +public class Lexer { + public static final char SINGLE_QUOTE = '\''; + public static final char DOUBLE_QUOTE = '\"'; + public static final char BEGIN_NODE_TYPE_NAME = '['; + public static final char END_NODE_TYPE_NAME = ']'; + public static final char EXTENDS = '>'; + public static final char LIST_DELIMITER = ','; + public static final char PROPERTY_DEFINITION = '-'; + public static final char CHILD_NODE_DEFINITION = '+'; + public static final char BEGIN_TYPE = '('; + public static final char END_TYPE = ')'; + public static final char DEFAULT = '='; + public static final char CONSTRAINT = '<'; + + public static final String[] ORDERABLE = new String[] {"orderable", "ord", "o"}; + public static final String[] MIXIN = new String[]{"mixin", "mix", "m"}; + + public static final String[] PRIMARY = new String[]{"primary", "pri", "!"}; + public static final String[] AUTOCREATED = new String[]{"autocreated", "aut", "a"}; + public static final String[] MANDATORY = new String[]{"mandatory", "man", "m"}; + public static final String[] PROTECTED = new String[]{"protected", "pro", "p"}; + public static final String[] MULTIPLE = new String[]{"multiple", "mul", "*"}; + + public static final String[] COPY = new String[]{"copy", "Copy", "COPY"}; + public static final String[] VERSION = new String[]{"version", "Version", "VERSION"}; + public static final String[] INITIALIZE = new String[]{"initialize", "Initialize", "INITIALIZE"}; + public static final String[] COMPUTE = new String[]{"compute", "Compute", "COMPUTE"}; + public static final String[] IGNORE = new String[]{"ignore", "Ignore", "IGNORE"}; + public static final String[] ABORT = new String[]{"abort", "Abort", "ABORT"}; + + public static final String[] ATTRIBUTE = new String[]{"primary", "pri", "!", + "autocreated", "aut", "a", + "mandatory", "man", "m", + "protected", "pro", "p", + "multiple", "mul", "*", + "copy", "Copy", "COPY", + "version", "Version", "VERSION", + "initialize", "Initialize", "INITIALIZE", + "compute", "Compute", "COMPUTE", + "ignore", "Ignore", "IGNORE", + "abort", "Abort", "ABORT"}; + + public static final String[] STRING = {"string", "String", "STRING"}; + public static final String[] BINARY = {"binary", "Binary", "BINARY"}; + public static final String[] LONG = {"long", "Long", "LONG"}; + public static final String[] DOUBLE = {"double", "Double", "DOUBLE"}; + public static final String[] BOOLEAN = {"boolean", "Boolean", "BOOLEAN"}; + public static final String[] DATE = {"date", "Date", "DATE"}; + public static final String[] NAME = {"name", "Name", "NAME"}; + public static final String[] PATH = {"path", "Path", "PATH"}; + public static final String[] REFERENCE = {"reference", "Reference", "REFERENCE"}; + + public static final String[] UNDEFINED = new String[]{"undefined", "Undefined", "UNDEFINED", "*"}; + + public static final String EOF = "eof"; + + private final StreamTokenizer st; + + private final String systemId; + + /** + * Constructor + * @param r + */ + public Lexer(Reader r, String systemId) { + this.systemId = systemId; + st = new StreamTokenizer(r); + + st.eolIsSignificant(false); + + st.lowerCaseMode(false); + + st.slashSlashComments(true); + st.slashStarComments(true); + + st.wordChars('a', 'z'); + st.wordChars('A', 'Z'); + st.wordChars(':', ':'); + st.wordChars('_', '_'); + + st.quoteChar(SINGLE_QUOTE); + st.quoteChar(DOUBLE_QUOTE); + + st.ordinaryChar(BEGIN_NODE_TYPE_NAME); + st.ordinaryChar(END_NODE_TYPE_NAME); + st.ordinaryChar(EXTENDS); + st.ordinaryChar(LIST_DELIMITER); + st.ordinaryChar(PROPERTY_DEFINITION); + st.ordinaryChar(CHILD_NODE_DEFINITION); + st.ordinaryChar(BEGIN_TYPE); + st.ordinaryChar(END_TYPE); + st.ordinaryChar(DEFAULT); + st.ordinaryChar(CONSTRAINT); + } + + /** + * getNextToken + * + * @return + * @throws ParseException + */ + public String getNextToken() throws ParseException { + try { + int tokenType = st.nextToken(); + if (tokenType == StreamTokenizer.TT_EOF) { + return EOF; + } else if (tokenType == StreamTokenizer.TT_WORD + || tokenType == SINGLE_QUOTE + || tokenType == DOUBLE_QUOTE) { + return st.sval; + } else if (tokenType == StreamTokenizer.TT_NUMBER) { + return String.valueOf(st.nval); + } else { + return new String(new char[] {(char) tokenType}); + } + } catch (IOException e) { + fail("IOException while attempting to read input stream", e); + return null; + } + } + + public void fail(String message) throws ParseException { + throw new ParseException(message, st.lineno(), -1, systemId); + } + + public void fail(String message, Throwable e) throws ParseException { + throw new ParseException(message, e, st.lineno(), -1, systemId); + } + + public void fail(Throwable e) throws ParseException { + throw new ParseException(e, st.lineno(), -1, systemId); + } +} Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefReader.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefReader.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefReader.java (revision 0) @@ -0,0 +1,709 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + +import java.io.Reader; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver; +import org.apache.jackrabbit.spi.commons.conversion.NameException; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceMapping; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; +import org.apache.jackrabbit.spi.commons.nodetype.compact.QNodeTypeDefinitionsBuilder.QNodeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.compact.QNodeTypeDefinitionsBuilder.QNodeTypeDefinitionBuilder; +import org.apache.jackrabbit.spi.commons.nodetype.compact.QNodeTypeDefinitionsBuilder.QPropertyDefinitionBuilder; +import org.apache.jackrabbit.util.ISO9075; + +/** + * CompactNodeTypeDefReader. Parses node type definitions written in the compact + * node type definition format and returns a list of QNodeTypeDefinition objects that + * can then be used to register node types. + *

+ * The EBNF grammar of the compact node type definition:
+ *

+ * cnd ::= ns_mapping* node_type_def+
+ *
+ * ns_mapping ::= "<" prefix "=" namespace ">"
+ *
+ * prefix ::= string
+ *
+ * namespace ::= string
+ *
+ * node_type_def ::= node_type_name [super_types] [options] {property_def | node_def}
+ *
+ * node_type_name ::= "[" string "]"
+ *
+ * super_types ::= ">" string_list
+ *
+ * options ::= orderable_opt | mixin_opt | orderable_opt mixin_opt | mixin_opt orderable_opt
+ *
+ * orderable_opt ::= "orderable" | "ord" | "o"
+ *
+ * mixin_opt ::= "mixin" | "mix" | "m"
+ *
+ * property_def ::= "-" property_name [property_type_decl] [default_values] [attributes] [value_constraints]
+ *
+ * property_name ::= string
+ *
+ * property_type_decl ::= "(" property_type ")"
+ *
+ * property_type ::= "STRING" | "String |"string" |
+ *                   "BINARY" | "Binary" | "binary" |
+ *                   "LONG" | "Long" | "long" |
+ *                   "DOUBLE" | "Double" | "double" |
+ *                   "BOOLEAN" | "Boolean" | "boolean" |
+ *                   "DATE" | "Date" | "date" |
+ *                   "NAME | "Name | "name |
+ *                   "PATH" | "Path" | "path" |
+ *                   "REFERENCE" | "Reference" | "reference" |
+ *                   "UNDEFINED" | "Undefined" | "undefined" | "*"
+ *
+ *
+ * default_values ::= "=" string_list
+ *
+ * value_constraints ::= "<" string_list
+ *
+ * node_def ::= "+" node_name [required_types] [default_type] [attributes]
+ *
+ * node_name ::= string
+ *
+ * required_types ::= "(" string_list ")"
+ *
+ * default_type ::= "=" string
+ *
+ * attributes ::= "primary" | "pri" | "!" |
+ *                "autocreated" | "aut" | "a" |
+ *                "mandatory" | "man" | "m" |
+ *                "protected" | "pro" | "p" |
+ *                "multiple" | "mul" | "*" |
+ *                "COPY" | "Copy" | "copy" |
+ *                "VERSION" | "Version" | "version" |
+ *                "INITIALIZE" | "Initialize" | "initialize" |
+ *                "COMPUTE" | "Compute" | "compute" |
+ *                "IGNORE" | "Ignore" | "ignore" |
+ *                "ABORT" | "Abort" | "abort"
+ *
+ * string_list ::= string {"," string}
+ *
+ * string ::= quoted_string | unquoted_string
+ *
+ * quoted_string :: = "'" unquoted_string "'"
+ *
+ * unquoted_string ::= [A-Za-z0-9:_]+
+ * 
+ */ +public class CompactNodeTypeDefReader { + + /** + * Empty array of value constraints + */ + private final static String[] EMPTY_VALUE_CONSTRAINTS = new String[0]; + + /** + * the list of parsed QNodeTypeDefinition + */ + private final List nodeTypeDefs = new LinkedList(); + + /** + * the current namespace mapping + */ + private final NamespaceMapping nsMapping; + + /** + * Name and Path resolver + */ + private final NamePathResolver resolver; + + /** + * the underlying lexer + */ + private final Lexer lexer; + + /** + * the current token + */ + private String currentToken; + + /** + * The builder for QNodeTypeDefinitions + */ + private final QNodeTypeDefinitionsBuilder builder; + + /** + * Creates a new CND reader. + * @param r + * @param systemId + * @param builder + * @throws ParseException + */ + public CompactNodeTypeDefReader(Reader r, String systemId, QNodeTypeDefinitionsBuilder builder) throws ParseException { + this(r, systemId, new NamespaceMapping(), builder); + } + + + /** + * Creates a new CND reader. + * @param r + * @param builder + * @throws ParseException + */ + public CompactNodeTypeDefReader(Reader r, String systemId, NamespaceMapping mapping, + QNodeTypeDefinitionsBuilder builder) throws ParseException { + + this.builder = builder; + lexer = new Lexer(r, systemId); + this.nsMapping = mapping; + this.resolver = new DefaultNamePathResolver(nsMapping); + nextToken(); + parse(); + } + + /** + * Returns the list of parsed QNodeTypeDefinition definitions. + * + * @return a List of QNodeTypeDefinition objects + */ + public List getNodeTypeDefs() { + return nodeTypeDefs; + } + + /** + * Returns the namespace mapping. + * + * @return a NamespaceMapping object. + */ + public NamespaceMapping getNamespaceMapping() { + return nsMapping; + } + + /** + * Parses the definition + * + * @throws ParseException + */ + private void parse() throws ParseException { + while (!currentTokenEquals(Lexer.EOF)) { + if (!doNameSpace()) { + break; + } + } + while (!currentTokenEquals(Lexer.EOF)) { + QNodeTypeDefinitionBuilder ntd = builder.newQNodeTypeDefinition(); + ntd.setOrderableChildNodes(false); + ntd.setMixin(false); + ntd.setPrimaryItemName(null); + doNodeTypeName(ntd); + doSuperTypes(ntd); + doOptions(ntd); + doItemDefs(ntd); + nodeTypeDefs.add(ntd.build()); + } + } + + + + /** + * processes the namespace declaration + * + * @return + * @throws ParseException + */ + private boolean doNameSpace() throws ParseException { + if (!currentTokenEquals('<')) { + return false; + } + nextToken(); + String prefix = currentToken; + nextToken(); + if (!currentTokenEquals('=')) { + lexer.fail("Missing = in namespace decl."); + } + nextToken(); + String uri = currentToken; + nextToken(); + if (!currentTokenEquals('>')) { + lexer.fail("Missing > in namespace decl."); + } + try { + nsMapping.setMapping(prefix, uri); + } catch (NamespaceException e) { + // ignore + } + nextToken(); + return true; + } + + /** + * processes the nodetype name + * + * @param ntd + * @throws ParseException + */ + private void doNodeTypeName(QNodeTypeDefinitionBuilder ntd) throws ParseException { + if (!currentTokenEquals(Lexer.BEGIN_NODE_TYPE_NAME)) { + lexer.fail("Missing '" + Lexer.BEGIN_NODE_TYPE_NAME + "' delimiter for beginning of node type name"); + } + nextToken(); + ntd.setName(toName(currentToken)); + + nextToken(); + if (!currentTokenEquals(Lexer.END_NODE_TYPE_NAME)) { + lexer.fail("Missing '" + Lexer.END_NODE_TYPE_NAME + "' delimiter for end of node type name, found " + currentToken); + } + nextToken(); + } + + /** + * processes the superclasses + * + * @param ntd + * @throws ParseException + */ + private void doSuperTypes(QNodeTypeDefinitionBuilder ntd) throws ParseException { + // a set would be nicer here, in case someone defines a supertype twice. + // but due to issue [JCR-333], the resulting node type definition is + // not symmetric anymore and the tests will fail. + ArrayList supertypes = new ArrayList(); + if (currentTokenEquals(Lexer.EXTENDS)) + do { + nextToken(); + supertypes.add(toName(currentToken)); + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + + ntd.setSupertypes((Name[]) supertypes.toArray(new Name[0])); + } + + /** + * processes the options + * + * @param ntd + * @throws ParseException + */ + private void doOptions(QNodeTypeDefinitionBuilder ntd) throws ParseException { + if (currentTokenEquals(Lexer.ORDERABLE)) { + ntd.setOrderableChildNodes(true); + nextToken(); + if (currentTokenEquals(Lexer.MIXIN)) { + ntd.setMixin(true); + nextToken(); + } + } else if (currentTokenEquals(Lexer.MIXIN)) { + ntd.setMixin(true); + nextToken(); + if (currentTokenEquals(Lexer.ORDERABLE)) { + ntd.setOrderableChildNodes(true); + nextToken(); + } + } + } + + /** + * processes the item definitions + * + * @param ntd + * @throws ParseException + */ + private void doItemDefs(QNodeTypeDefinitionBuilder ntd) throws ParseException { + List propertyDefinitions = new ArrayList(); + List nodeDefinitions = new ArrayList(); + while (currentTokenEquals(Lexer.PROPERTY_DEFINITION) || currentTokenEquals(Lexer.CHILD_NODE_DEFINITION)) { + if (currentTokenEquals(Lexer.PROPERTY_DEFINITION)) { + QPropertyDefinitionBuilder pd = ntd.newQPropertyDefinition(); + + pd.setAutoCreated(false); + pd.setDeclaringNodeType(ntd.getName()); + pd.setDefaultValues(null); + pd.setMandatory(false); + pd.setMultiple(false); + pd.setOnParentVersion(OnParentVersionAction.COPY); + pd.setProtected(false); + pd.setRequiredType(PropertyType.STRING); + pd.setValueConstraints(EMPTY_VALUE_CONSTRAINTS); + + nextToken(); + doPropertyDefinition(pd, ntd); + propertyDefinitions.add(pd.build()); + + } else if (currentTokenEquals(Lexer.CHILD_NODE_DEFINITION)) { + QNodeDefinitionBuilder nd = ntd.newQNodeDefinitionBuilder(); + + nd.setAllowsSameNameSiblings(false); + nd.setAutoCreated(false); + nd.setDeclaringNodeType(ntd.getName()); + nd.setMandatory(false); + nd.setOnParentVersion(OnParentVersionAction.COPY); + nd.setProtected(false); + nd.setDefaultPrimaryType(null); + nd.setRequiredPrimaryTypes(new Name[]{NameConstants.NT_BASE}); + + nextToken(); + doChildNodeDefinition(nd, ntd); + nodeDefinitions.add(nd.build()); + } + } + + ntd.setPropertyDefs((QPropertyDefinition[]) propertyDefinitions + .toArray(new QPropertyDefinition[0])); + + ntd.setChildNodeDefs((QNodeDefinition[]) nodeDefinitions.toArray(new QNodeDefinition[0])); + } + + /** + * processes the property definition + * + * @param pd + * @param ntd + * @throws ParseException + */ + private void doPropertyDefinition(QPropertyDefinitionBuilder pd, QNodeTypeDefinitionBuilder ntd) + throws ParseException { + if (currentToken.equals("*")) { + pd.setName(NameConstants.ANY_NAME); + } else { + pd.setName(toName(currentToken)); + } + nextToken(); + doPropertyType(pd); + doPropertyDefaultValue(pd); + doPropertyAttributes(pd, ntd); + doPropertyValueConstraints(pd); + } + + /** + * processes the property type + * + * @param pd + * @throws ParseException + */ + private void doPropertyType(QPropertyDefinitionBuilder pd) throws ParseException { + if (!currentTokenEquals(Lexer.BEGIN_TYPE)) { + return; + } + nextToken(); + if (currentTokenEquals(Lexer.STRING)) { + pd.setRequiredType(PropertyType.STRING); + } else if (currentTokenEquals(Lexer.BINARY)) { + pd.setRequiredType(PropertyType.BINARY); + } else if (currentTokenEquals(Lexer.LONG)) { + pd.setRequiredType(PropertyType.LONG); + } else if (currentTokenEquals(Lexer.DOUBLE)) { + pd.setRequiredType(PropertyType.DOUBLE); + } else if (currentTokenEquals(Lexer.BOOLEAN)) { + pd.setRequiredType(PropertyType.BOOLEAN); + } else if (currentTokenEquals(Lexer.DATE)) { + pd.setRequiredType(PropertyType.DATE); + } else if (currentTokenEquals(Lexer.NAME)) { + pd.setRequiredType(PropertyType.NAME); + } else if (currentTokenEquals(Lexer.PATH)) { + pd.setRequiredType(PropertyType.PATH); + } else if (currentTokenEquals(Lexer.REFERENCE)) { + pd.setRequiredType(PropertyType.REFERENCE); + } else if (currentTokenEquals(Lexer.UNDEFINED)) { + pd.setRequiredType(PropertyType.UNDEFINED); + } else { + lexer.fail("Unkown property type '" + currentToken + "' specified"); + } + nextToken(); + if (!currentTokenEquals(Lexer.END_TYPE)) { + lexer.fail("Missing '" + Lexer.END_TYPE + "' delimiter for end of property type"); + } + nextToken(); + } + + /** + * processes the property attributes + * + * @param pd + * @param ntd + * @throws ParseException + */ + private void doPropertyAttributes(QPropertyDefinitionBuilder pd, QNodeTypeDefinitionBuilder ntd) throws ParseException { + while (currentTokenEquals(Lexer.ATTRIBUTE)) { + if (currentTokenEquals(Lexer.PRIMARY)) { + if (ntd.getPrimaryItemName() != null) { + String name = null; + try { + name = resolver.getJCRName(ntd.getName()); + } catch (NamespaceException e) { + // Should never happen, checked earlier + } + lexer.fail("More than one primary item specified in node type '" + name + "'"); + } + ntd.setPrimaryItemName(pd.getName()); + } else if (currentTokenEquals(Lexer.AUTOCREATED)) { + pd.setAutoCreated(true); + } else if (currentTokenEquals(Lexer.MANDATORY)) { + pd.setMandatory(true); + } else if (currentTokenEquals(Lexer.PROTECTED)) { + pd.setProtected(true); + } else if (currentTokenEquals(Lexer.MULTIPLE)) { + pd.setMultiple(true); + } else if (currentTokenEquals(Lexer.COPY)) { + pd.setOnParentVersion(OnParentVersionAction.COPY); + } else if (currentTokenEquals(Lexer.VERSION)) { + pd.setOnParentVersion(OnParentVersionAction.VERSION); + } else if (currentTokenEquals(Lexer.INITIALIZE)) { + pd.setOnParentVersion(OnParentVersionAction.INITIALIZE); + } else if (currentTokenEquals(Lexer.COMPUTE)) { + pd.setOnParentVersion(OnParentVersionAction.COMPUTE); + } else if (currentTokenEquals(Lexer.IGNORE)) { + pd.setOnParentVersion(OnParentVersionAction.IGNORE); + } else if (currentTokenEquals(Lexer.ABORT)) { + pd.setOnParentVersion(OnParentVersionAction.ABORT); + } + nextToken(); + } + } + + /** + * processes the property default values + * + * @param pd + * @throws ParseException + */ + private void doPropertyDefaultValue(QPropertyDefinitionBuilder pd) throws ParseException { + if (!currentTokenEquals(Lexer.DEFAULT)) { + return; + } + List defaultValues = new ArrayList(); + do { + nextToken(); + QValue value = null; + try { + value = pd.createValue(currentToken, resolver); + } catch (ValueFormatException e) { + lexer.fail("'" + currentToken + "' is not a valid string representation of a value of type " + pd.getRequiredType()); + } catch (RepositoryException e) { + lexer.fail("An error occured during value conversion of '" + currentToken + "'"); + } + defaultValues.add(value); + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + pd.setDefaultValues((QValue[]) defaultValues.toArray(new QValue[0])); + } + + /** + * processes the property value constraints + * + * @param pd + * @throws ParseException + */ + private void doPropertyValueConstraints(QPropertyDefinitionBuilder pd) throws ParseException { + if (!currentTokenEquals(Lexer.CONSTRAINT)) { + return; + } + List constraints = new ArrayList(); + do { + nextToken(); + String constraint = null; + try { + constraint = pd.createValueConstraint(currentToken, resolver); + } catch (InvalidConstraintException e) { + lexer.fail("'" + currentToken + "' is not a valid constraint expression for a value of type " + pd.getRequiredType()); + } + constraints.add(constraint); + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + pd.setValueConstraints((String[]) constraints.toArray(new String[0])); + } + + /** + * processes the childnode definition + * + * @param nd + * @param ntd + * @throws ParseException + */ + private void doChildNodeDefinition(QNodeDefinitionBuilder nd, QNodeTypeDefinitionBuilder ntd) + throws ParseException { + if (currentTokenEquals('*')) { + nd.setName(NameConstants.ANY_NAME); + } else { + nd.setName(toName(currentToken)); + } + nextToken(); + doChildNodeRequiredTypes(nd); + doChildNodeDefaultType(nd); + doChildNodeAttributes(nd, ntd); + } + + /** + * processes the childnode required types + * + * @param nd + * @throws ParseException + */ + private void doChildNodeRequiredTypes(QNodeDefinitionBuilder nd) throws ParseException { + if (!currentTokenEquals(Lexer.BEGIN_TYPE)) { + return; + } + List types = new ArrayList(); + do { + nextToken(); + types.add(toName(currentToken)); + nextToken(); + } while (currentTokenEquals(Lexer.LIST_DELIMITER)); + nd.setRequiredPrimaryTypes((Name[]) types.toArray(new Name[0])); + nextToken(); + } + + /** + * processes the childnode default types + * + * @param nd + * @throws ParseException + */ + private void doChildNodeDefaultType(QNodeDefinitionBuilder nd) throws ParseException { + if (!currentTokenEquals(Lexer.DEFAULT)) { + return; + } + nextToken(); + nd.setDefaultPrimaryType(toName(currentToken)); + nextToken(); + } + + /** + * processes the childnode attributes + * + * @param nd + * @param ntd + * @throws ParseException + */ + private void doChildNodeAttributes(QNodeDefinitionBuilder nd, QNodeTypeDefinitionBuilder ntd) throws ParseException { + while (currentTokenEquals(Lexer.ATTRIBUTE)) { + if (currentTokenEquals(Lexer.PRIMARY)) { + if (ntd.getPrimaryItemName() != null) { + String name = null; + try { + name = resolver.getJCRName(ntd.getName()); + } catch (NamespaceException e) { + // Should never happen, checked earlier + } + lexer.fail("More than one primary item specified in node type '" + name + "'"); + } + ntd.setPrimaryItemName(nd.getName()); + } else if (currentTokenEquals(Lexer.AUTOCREATED)) { + nd.setAutoCreated(true); + } else if (currentTokenEquals(Lexer.MANDATORY)) { + nd.setMandatory(true); + } else if (currentTokenEquals(Lexer.PROTECTED)) { + nd.setProtected(true); + } else if (currentTokenEquals(Lexer.MULTIPLE)) { + nd.setAllowsSameNameSiblings(true); + } else if (currentTokenEquals(Lexer.COPY)) { + nd.setOnParentVersion(OnParentVersionAction.COPY); + } else if (currentTokenEquals(Lexer.VERSION)) { + nd.setOnParentVersion(OnParentVersionAction.VERSION); + } else if (currentTokenEquals(Lexer.INITIALIZE)) { + nd.setOnParentVersion(OnParentVersionAction.INITIALIZE); + } else if (currentTokenEquals(Lexer.COMPUTE)) { + nd.setOnParentVersion(OnParentVersionAction.COMPUTE); + } else if (currentTokenEquals(Lexer.IGNORE)) { + nd.setOnParentVersion(OnParentVersionAction.IGNORE); + } else if (currentTokenEquals(Lexer.ABORT)) { + nd.setOnParentVersion(OnParentVersionAction.ABORT); + } + nextToken(); + } + } + + /** + * Converts the given string into a qualified name using the current + * namespace mapping. + * + * @param stringName + * @return the qualified name + * @throws ParseException if the conversion fails + */ + private Name toName(String stringName) throws ParseException { + try { + Name n = resolver.getQName(stringName); + String decodedLocalName = ISO9075.decode(n.getLocalName()); + return builder.createName(n.getNamespaceURI(), decodedLocalName); + } catch (NameException e) { + lexer.fail("Error while parsing '" + stringName + "'", e); + return null; + } catch (NamespaceException e) { + lexer.fail("Error while parsing '" + stringName + "'", e); + return null; + } + } + + /** + * Gets the next token from the underlying lexer. + * + * @see Lexer#getNextToken() + * @throws ParseException if the lexer fails to get the next token. + */ + private void nextToken() throws ParseException { + currentToken = lexer.getNextToken(); + } + + /** + * Checks if the {@link #currentToken} is semantically equal to the given + * argument. + * + * @param s the tokens to compare with + * @return true if equals; false otherwise. + */ + private boolean currentTokenEquals(String[] s) { + for (int i = 0; i < s.length; i++) { + if (currentToken.equals(s[i])) { + return true; + } + } + return false; + } + + /** + * Checks if the {@link #currentToken} is semantically equal to the given + * argument. + * + * @param c the tokens to compare with + * @return true if equals; false otherwise. + */ + private boolean currentTokenEquals(char c) { + return currentToken.length() == 1 && currentToken.charAt(0) == c; + } + + /** + * Checks if the {@link #currentToken} is semantically equal to the given + * argument. + * + * @param s the tokens to compare with + * @return true if equals; false otherwise. + */ + private boolean currentTokenEquals(String s) { + return currentToken.equals(s); + } + +} Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/ParseException.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/ParseException.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/ParseException.java (revision 0) @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + +/** + * ParseException + */ +public class ParseException extends Exception { + + /** + * the line number where the error occurred + */ + private final int lineNumber; + + /** + * the column number where the error occurred + */ + private final int colNumber; + + /** + * the systemid of the source that produced the error + */ + private final String systemId; + + + /** + * Constructs a new instance of this class with null as its + * detail message. + */ + public ParseException(int lineNumber, int colNumber, String systemId) { + super(); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * Constructs a new instance of this class with the specified detail + * message. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + */ + public ParseException(String message, int lineNumber, int colNumber, String systemId) { + super(message); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * Constructs a new instance of this class with the specified detail + * message and root cause. + * + * @param message the detail message. The detail message is saved for + * later retrieval by the {@link #getMessage()} method. + * @param rootCause root failure cause + */ + public ParseException(String message, Throwable rootCause, int lineNumber, int colNumber, String systemId) { + super(message, rootCause); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * Constructs a new instance of this class with the specified root cause. + * + * @param rootCause root failure cause + */ + public ParseException(Throwable rootCause, int lineNumber, int colNumber, String systemId) { + super(rootCause); + this.lineNumber = lineNumber; + this.colNumber = colNumber; + this.systemId = systemId; + } + + /** + * {@inheritDoc} + */ + public String getMessage() { + StringBuffer b = new StringBuffer(super.getMessage()); + String delim = " ("; + if (systemId != null && !systemId.equals("")) { + b.append(delim); + b.append(systemId); + delim = ", "; + } + if (lineNumber >= 0) { + b.append(delim); + b.append("line "); + b.append(lineNumber); + delim = ", "; + } + if (colNumber >= 0) { + b.append(delim); + b.append("col "); + b.append(colNumber); + delim = ", "; + } + if (delim.equals(", ")) { + b.append(")"); + } + return b.toString(); + } + + /** + * {@inheritDoc} + */ + public String toString() { + return super.toString(); // + " (" + systemId + ", line " + lineNumber +", col " + colNumber +")"; + } + +} Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefWriter.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefWriter.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/CompactNodeTypeDefWriter.java (revision 0) @@ -0,0 +1,470 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import javax.jcr.NamespaceException; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.ValueFactory; +import javax.jcr.version.OnParentVersionAction; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameConstants; +import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; +import org.apache.jackrabbit.util.ISO9075; + +/** + * Prints node type defs in a compact notation + * Print Format: + * + * [ex:NodeType] > ex:ParentType1, ex:ParentType2 + * orderable mixin + * - ex:property (STRING) = 'default1', 'default2' + * primary mandatory autocreated protected multiple VERSION + * < 'constraint1', 'constraint2' + * + ex:node (ex:reqType1, ex:reqType2) = ex:defaultType + * mandatory autocreated protected multiple VERSION + */ +public class CompactNodeTypeDefWriter { + + /** + * the indention string + */ + private static final String INDENT = " "; + + /** + * the current namespace resolver + */ + private final NamespaceResolver resolver; + + /** + * the current name/path resolver + */ + private final NamePathResolver npResolver; + + /** + * the current value factory + */ + private final ValueFactory valueFactory; + + /** + * the underlying writer + */ + private Writer out; + + /** + * special writer used for namespaces + */ + private Writer nsWriter; + + /** + * namespaces(prefixes) that are used + */ + private final HashSet usedNamespaces = new HashSet(); + + /** + * Creates a new nodetype writer + * + * @param out the underlying writer + * @param r the namespace resolver + * @param npResolver + * @param valueFactory + */ + public CompactNodeTypeDefWriter(Writer out, NamespaceResolver r, NamePathResolver npResolver, + ValueFactory valueFactory) { + this(out, r, npResolver, valueFactory, false); + } + + /** + * Creates a new nodetype writer + * + * @param out the underlaying writer + * @param r the naespace resolver + * @param npResolver + * @param valueFactory + * @param includeNS if true all used namespace decl. are also + */ + public CompactNodeTypeDefWriter(Writer out, NamespaceResolver r, NamePathResolver npResolver, + ValueFactory valueFactory, boolean includeNS) { + this.resolver = r; + this.npResolver = npResolver; + this.valueFactory = valueFactory; + if (includeNS) { + this.out = new StringWriter(); + this.nsWriter = out; + } else { + this.out = out; + this.nsWriter = null; + } + } + + /** + * Writes the given list of QNodeTypeDefinition to the output writer including the + * used namespaces. + * + * @param l + * @param r + * @param npResolver + * @param valueFactory + * @param out + * @throws IOException + */ + public static void write(List l, NamespaceResolver r, NamePathResolver npResolver, + ValueFactory valueFactory, Writer out) + throws IOException { + CompactNodeTypeDefWriter w = new CompactNodeTypeDefWriter(out, r, npResolver, valueFactory, true); + Iterator iter = l.iterator(); + while (iter.hasNext()) { + QNodeTypeDefinition def = (QNodeTypeDefinition) iter.next(); + w.write(def); + } + w.close(); + } + + /** + * Write one QNodeTypeDefinition to this writer + * + * @param ntd + * @throws IOException + */ + public void write(QNodeTypeDefinition ntd) throws IOException { + writeName(ntd); + writeSupertypes(ntd); + writeOptions(ntd); + writePropDefs(ntd); + writeNodeDefs(ntd); + out.write("\n\n"); + } + + /** + * Flushes all pending write operations and Closes this writer. please note, + * that the underlying writer remains open. + * + * @throws IOException + */ + public void close() throws IOException { + if (nsWriter != null) { + nsWriter.write("\n"); + out.close(); + nsWriter.write(((StringWriter) out).getBuffer().toString()); + out = nsWriter; + nsWriter = null; + } + out.flush(); + out = null; + } + + /** + * write name + */ + private void writeName(QNodeTypeDefinition ntd) throws IOException { + out.write("["); + out.write(resolve(ntd.getName())); + out.write("]"); + } + + /** + * write supertypes + */ + private void writeSupertypes(QNodeTypeDefinition ntd) throws IOException { + Name[] sta = ntd.getSupertypes(); + String delim = " > "; + for (int i = 0; i < sta.length; i++) { + out.write(delim); + out.write(resolve(sta[i])); + delim = ", "; + } + } + + /** + * write options + */ + private void writeOptions(QNodeTypeDefinition ntd) throws IOException { + if (ntd.hasOrderableChildNodes()) { + out.write("\n" + INDENT); + out.write("orderable"); + if (ntd.isMixin()) { + out.write(" mixin"); + } + } else if (ntd.isMixin()) { + out.write("\n" + INDENT); + out.write("mixin"); + } + } + + /** + * write prop defs + */ + private void writePropDefs(QNodeTypeDefinition ntd) throws IOException { + QPropertyDefinition[] pda = ntd.getPropertyDefs(); + for (int i = 0; i < pda.length; i++) { + QPropertyDefinition pd = pda[i]; + writePropDef(ntd, pd); + } + } + + /** + * write node defs + */ + private void writeNodeDefs(QNodeTypeDefinition ntd) throws IOException { + QNodeDefinition[] nda = ntd.getChildNodeDefs(); + for (int i = 0; i < nda.length; i++) { + QNodeDefinition nd = nda[i]; + writeNodeDef(ntd, nd); + } + } + + /** + * write prop def + * @param pd + */ + private void writePropDef(QNodeTypeDefinition ntd, QPropertyDefinition pd) throws IOException { + out.write("\n" + INDENT + "- "); + + Name name = pd.getName(); + if (name.equals(NameConstants.ANY_NAME)) { + out.write('*'); + } else { + writeItemDefName(name); + } + + out.write(" ("); + out.write(PropertyType.nameFromValue(pd.getRequiredType()).toLowerCase()); + out.write(")"); + writeDefaultValues(pd.getDefaultValues()); + out.write(ntd.getPrimaryItemName() != null && ntd.getPrimaryItemName().equals(pd.getName()) ? " primary" : ""); + if (pd.isMandatory()) { + out.write(" mandatory"); + } + if (pd.isAutoCreated()) { + out.write(" autocreated"); + } + if (pd.isProtected()) { + out.write(" protected"); + } + if (pd.isMultiple()) { + out.write(" multiple"); + } + if (pd.getOnParentVersion() != OnParentVersionAction.COPY) { + out.write(" "); + out.write(OnParentVersionAction.nameFromValue(pd.getOnParentVersion()).toLowerCase()); + } + writeValueConstraints(pd.getValueConstraints(), pd.getRequiredType()); + } + + /** + * write default values + * @param dva + */ + private void writeDefaultValues(QValue[] dva) throws IOException { + if (dva != null && dva.length > 0) { + String delim = " = '"; + for (int i = 0; i < dva.length; i++) { + out.write(delim); + try { + Value v = ValueFormat.getJCRValue(dva[i], npResolver, valueFactory); + out.write(escape(v.getString())); + } catch (RepositoryException e) { + out.write(escape(dva[i].toString())); + } + out.write("'"); + delim = ", '"; + } + } + } + + /** + * write value constraints + * @param vca + */ + private void writeValueConstraints(String[] vca, int type) throws IOException { + if (vca != null && vca.length > 0) { + String vc = convertConstraint(vca[0], type); + out.write(" < '"); + out.write(escape(vc)); + out.write("'"); + for (int i = 1; i < vca.length; i++) { + vc = convertConstraint(vca[i], type); + out.write(", '"); + out.write(escape(vc)); + out.write("'"); + } + } + } + + private String convertConstraint(String vc, int type) { + if (type == PropertyType.REFERENCE || type == PropertyType.NAME || type == PropertyType.PATH) { + if (type == PropertyType.REFERENCE) + type = PropertyType.NAME; + + try { + QValue qv = QValueFactoryImpl.getInstance().create(vc, type); + vc = ValueFormat.getJCRValue(qv, npResolver, valueFactory).getString(); + } + catch (RepositoryException e) { + // ignore -> return unconverted constraint + } + } + + return vc; + } + + /** + * write node def + * + * @param nd + */ + private void writeNodeDef(QNodeTypeDefinition ntd, QNodeDefinition nd) throws IOException { + out.write("\n" + INDENT + "+ "); + + Name name = nd.getName(); + if (name.equals(NameConstants.ANY_NAME)) { + out.write('*'); + } else { + writeItemDefName(name); + } + writeRequiredTypes(nd.getRequiredPrimaryTypes()); + writeDefaultType(nd.getDefaultPrimaryType()); + out.write(ntd.getPrimaryItemName() != null && ntd.getPrimaryItemName().equals(nd.getName()) ? " primary" : ""); + if (nd.isMandatory()) { + out.write(" mandatory"); + } + if (nd.isAutoCreated()) { + out.write(" autocreated"); + } + if (nd.isProtected()) { + out.write(" protected"); + } + if (nd.allowsSameNameSiblings()) { + out.write(" multiple"); + } + if (nd.getOnParentVersion() != OnParentVersionAction.COPY) { + out.write(" "); + out.write(OnParentVersionAction.nameFromValue(nd.getOnParentVersion()).toLowerCase()); + } + } + + /** + * Write item def name + * @param name + * @throws IOException + */ + private void writeItemDefName(Name name) throws IOException { + out.write(resolve(name)); + } + /** + * write required types + * @param reqTypes + */ + private void writeRequiredTypes(Name[] reqTypes) throws IOException { + if (reqTypes != null && reqTypes.length > 0) { + String delim = " ("; + for (int i = 0; i < reqTypes.length; i++) { + out.write(delim); + out.write(resolve(reqTypes[i])); + delim = ", "; + } + out.write(")"); + } + } + + /** + * write default types + * @param defType + */ + private void writeDefaultType(Name defType) throws IOException { + if (defType != null && !defType.getLocalName().equals("*")) { + out.write(" = "); + out.write(resolve(defType)); + } + } + + /** + * resolve + * @param name + * @return the resolved name + */ + private String resolve(Name name) throws IOException { + if (name == null) { + return ""; + } + try { + String prefix = resolver.getPrefix(name.getNamespaceURI()); + if (prefix != null && !prefix.equals(Name.NS_EMPTY_PREFIX)) { + // check for writing namespaces + if (nsWriter != null) { + if (!usedNamespaces.contains(prefix)) { + usedNamespaces.add(prefix); + nsWriter.write("<'"); + nsWriter.write(prefix); + nsWriter.write("'='"); + nsWriter.write(escape(name.getNamespaceURI())); + nsWriter.write("'>\n"); + } + } + prefix += ":"; + } + + String encLocalName = ISO9075.encode(name.getLocalName()); + String resolvedName = prefix + encLocalName; + + // check for '-' and '+' + if (resolvedName.indexOf('-') >= 0 || resolvedName.indexOf('+') >= 0) { + return "'" + resolvedName + "'"; + } else { + return resolvedName; + } + + } catch (NamespaceException e) { + return name.toString(); + } + } + + /** + * escape + * @param s + * @return the escaped string + */ + private String escape(String s) { + StringBuffer sb = new StringBuffer(s); + for (int i = 0; i < sb.length(); i++) { + if (sb.charAt(i) == '\\') { + sb.insert(i, '\\'); + i++; + } else if (sb.charAt(i) == '\'') { + sb.insert(i, '\''); + i++; + } + } + return sb.toString(); + } +} Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/QNodeTypeDefinitionsBuilder.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/QNodeTypeDefinitionsBuilder.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/QNodeTypeDefinitionsBuilder.java (revision 0) @@ -0,0 +1,453 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.QItemDefinition; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; + +/** + * A builder for {@link QNodeTypeDefinition}s + */ +public abstract class QNodeTypeDefinitionsBuilder { + + /** + * @return a new instance of a builder for a {@link QNodeTypeDefinition} + */ + public abstract QNodeTypeDefinitionBuilder newQNodeTypeDefinition(); + + /** + * Returns a Name with the given namespace URI and + * local part and validates the given parameters. + * + * @param namespaceURI namespace uri + * @param localName local part + * @throws IllegalArgumentException if namespaceURI or + * localName is invalid. + */ + public abstract Name createName(String namespaceURI, String localName) throws IllegalArgumentException; + + /** + * A builder for a {@link QNodeTypeDefinition} + */ + public abstract class QNodeTypeDefinitionBuilder { + private Name name; + private Name[] supertypes; + private boolean isMixin; + private boolean isOrderable; + private Name primaryItemName; + private QPropertyDefinition[] propertyDefinitions; + private QNodeDefinition[] childNodeDefinitions; + + /** + * Set the name of the node type definition being built + * @param name + */ + public void setName(Name name) { + this.name = name; + } + + /** + * @return the name of the node type definition being built or null if not set. + */ + public Name getName() { + return name; + } + + /** + * Specifies the supertypes of the node type definition being built + * @param supertypes + */ + public void setSupertypes(Name[] supertypes) { + this.supertypes = supertypes; + } + + /** + * Returns an array containing the names of the supertypes of the node type definition being + * built. + * + * @return an array of supertype names + */ + public Name[] getSuperTypes() { + return supertypes; + } + + /** + * @param isMixin true if building a mixin node type definition; false otherwise. + */ + public void setMixin(boolean isMixin) { + this.isMixin = isMixin; + } + + /** + * @return true if building a mixin node type definition; false otherwise. + */ + public boolean getMixin() { + return isMixin; + } + + /** + * @param isOrderable true if building a node type having orderable child nodes; false + * otherwise. + */ + public void setOrderableChildNodes(boolean isOrderable) { + this.isOrderable = isOrderable; + } + + /** + * @return true if building a node type having orderable child nodes; false otherwise. + */ + public boolean getOrderableChildNodes() { + return isOrderable; + } + + /** + * @param primaryItemName the name of the primary item or null if not set. + */ + public void setPrimaryItemName(Name primaryItemName) { + this.primaryItemName = primaryItemName; + } + + /** + * @return the name of the primary item or null if not set. + */ + public Name getPrimaryItemName() { + return primaryItemName; + } + + /** + * @param propDefs an array containing the property definitions of the node type definition + * being built. + */ + public void setPropertyDefs(QPropertyDefinition[] propDefs) { + propertyDefinitions = propDefs; + } + + /** + * @return an array containing the property definitions of the node type definition being + * built or null if not set. + */ + public QPropertyDefinition[] getPropertyDefs() { + return propertyDefinitions; + } + + /** + * @param childDefs an array containing the child node definitions of the node type + * definition being. + */ + public void setChildNodeDefs(QNodeDefinition[] childDefs) { + childNodeDefinitions = childDefs; + } + + /** + * @return an array containing the child node definitions of the node type definition being + * built or null if not set. + */ + public QNodeDefinition[] getChildNodeDefs() { + return childNodeDefinitions; + } + + /** + * @return a new instance of a builder for a {@link QNodeDefinition}. + */ + public abstract QPropertyDefinitionBuilder newQPropertyDefinition(); + + /** + * @return a new instance of a builder for a {@link QNodeDefinition}. + */ + public abstract QNodeDefinitionBuilder newQNodeDefinitionBuilder(); + + /** + * Creates a new {@link QNodeTypeDefinition} instance based on the state of this builder. + * + * @return a new {@link QNodeTypeDefinition} instance. + * @throws IllegalStateException if the instance has not the necessary information to build + * the QNodeTypeDefinition instance. + */ + public abstract QNodeTypeDefinition build() throws IllegalStateException; + } + + /** + * A builder for a {@link QItemDefinition} + */ + abstract class QItemDefinitionBuilder { + private Name name; + private Name declaringType; + private boolean isAutocreated; + private int onParentVersion; + private boolean isProtected; + private boolean isMandatory; + + /** + * @param name the name of the child item definition being build + */ + public void setName(Name name) { + this.name = name; + } + + /** + * @return the name of the child item definition being build. + */ + public Name getName() { + return name; + } + + /** + * @param type the name of the declaring node type. + */ + public void setDeclaringNodeType(Name type) { + declaringType = type; + } + + /** + * @return the name of the declaring node type. + */ + public Name getDeclaringNodeType() { + return declaringType; + } + + /** + * @param autocreate true if building a 'autocreate' child item definition, false otherwise. + */ + public void setAutoCreated(boolean autocreate) { + isAutocreated = autocreate; + } + + /** + * @return true if building a 'autocreate' child item definition, false otherwise. + */ + public boolean getAutoCreated() { + return isAutocreated; + } + + /** + * @param onParent the 'onParentVersion' attribute of the child item definition being built + */ + public void setOnParentVersion(int onParent) { + onParentVersion = onParent; + } + + /** + * @return the 'onParentVersion' attribute of the child item definition being built + */ + public int getOnParentVersion() { + return onParentVersion; + } + + /** + * @param isProtected true if building a 'protected' child item definition, false otherwise. + */ + public void setProtected(boolean isProtected) { + this.isProtected = isProtected; + } + + /** + * @return true if building a 'protected' child item definition, false otherwise. + */ + public boolean getProtected() { + return isProtected; + } + + /** + * @param isMandatory true if building a 'mandatory' child item definition, false otherwise. + */ + public void setMandatory(boolean isMandatory) { + this.isMandatory = isMandatory; + } + + /** + * @return true if building a 'mandatory' child item definition, false otherwise. + */ + public boolean getMandatory() { + return isMandatory; + } + } + + /** + * A builder for a {@link QNodeDefinition} + */ + public abstract class QPropertyDefinitionBuilder extends QItemDefinitionBuilder { + private int requiredType; + private String[] valueConstraints; + private QValue[] defaultValues; + private boolean isMultiple; + + /** + * @param type the required type of the property definition being built. + */ + public void setRequiredType(int type) { + requiredType = type; + } + + /** + * @return the required type of the property definition being built. + */ + public int getRequiredType() { + return requiredType; + } + + /** + * @param constraints array of value constraints of the property definition being built. + */ + public void setValueConstraints(String[] constraints) { + valueConstraints = constraints; + } + + /** + * @return array of value constraints of the property definition being built. + */ + public String[] getValueConstraints() { + return valueConstraints; + } + + /** + * @param values array of default values of the property definition being built. + */ + public void setDefaultValues(QValue[] values) { + defaultValues = values; + } + + /** + * @return array of default values of the property definition being built or + * null if no default values are defined. + */ + public QValue[] getDefaultValues() { + return defaultValues; + } + + /** + * @param isMultiple true if building a 'multiple' property definition. + */ + public void setMultiple(boolean isMultiple) { + this.isMultiple = isMultiple; + } + + /** + * @return true if building a 'multiple' property definition. + */ + public boolean getMultiple() { + return isMultiple; + } + + /** + * Validate the given constraint and resolve any prefixes. + * + * @param constraint + * @param resolver + * @return A syntactically valid value constrained which refers to fully qualified names and + * paths only. + * @throws InvalidConstraintException if constraint cannot be converted to a + * valid value constrained. + */ + public abstract String createValueConstraint(String constraint, NamePathResolver resolver) + throws InvalidConstraintException; + + /** + * Create a new QValue for value of the type this instance + * represents using the given resolver. + * + * @param value + * @param resolver + * @return a new QValue. + * @throws ValueFormatException If the given value cannot be converted to the + * specified type. + * @throws RepositoryException If another error occurs. + */ + public abstract QValue createValue(String value, NamePathResolver resolver) + throws ValueFormatException, RepositoryException; + + /** + * Creates a new {@link QPropertyDefinition} instance based on the state of this builder. + * + * @return a new {@link QPropertyDefinition} instance. + * @throws IllegalStateException if the instance has not the necessary information to build + * the QPropertyDefinition instance. + */ + public abstract QPropertyDefinition build() throws IllegalStateException; + } + + /** + * A builder for a {@link QNodeDefinition} + */ + public abstract class QNodeDefinitionBuilder extends QItemDefinitionBuilder { + private Name defaultPrimaryType; + private Name[] requiredPrimaryTypes; + private boolean allowsSameNameSiblings; + + /** + * @param name the name of the default primary type of the node definition being built. + */ + public void setDefaultPrimaryType(Name name) { + defaultPrimaryType = name; + } + + /** + * @return the name of the default primary type of the node definition being built. + */ + public Name getDefaultPrimaryType() { + return defaultPrimaryType; + } + + /** + * @param names array of names of the required primary types of the node definition being + * built. + */ + public void setRequiredPrimaryTypes(Name[] names) { + requiredPrimaryTypes = names; + } + + /** + * @return array of names of the required primary types of the node definition being built. + */ + public Name[] getRequiredPrimaryTypes() { + return requiredPrimaryTypes; + } + + /** + * @param allowSns true if building a node definition with same name siblings, false + * otherwise. + */ + public void setAllowsSameNameSiblings(boolean allowSns) { + allowsSameNameSiblings = allowSns; + } + + /** + * @return true if building a node definition with same name siblings, false otherwise. + */ + public boolean getAllowsSameNameSiblings() { + return allowsSameNameSiblings; + } + + /** + * Creates a new {@link QNodeDefinition} instance based on the state of this builder. + * + * @return a new {@link QNodeDefinition} instance. + * @throws IllegalStateException if the instance has not the necessary information to build + * the QNodeDefinition instance. + */ + public abstract QNodeDefinition build() throws IllegalStateException; + } + +} Index: src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/QNodeTypeDefinitionsBuilderImpl.java =================================================================== --- src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/QNodeTypeDefinitionsBuilderImpl.java (revision 0) +++ src/main/java/org/apache/jackrabbit/spi/commons/nodetype/compact/QNodeTypeDefinitionsBuilderImpl.java (revision 0) @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.jackrabbit.spi.commons.nodetype.compact; + +import javax.jcr.RepositoryException; +import javax.jcr.ValueFormatException; + +import org.apache.jackrabbit.spi.Name; +import org.apache.jackrabbit.spi.NameFactory; +import org.apache.jackrabbit.spi.QNodeDefinition; +import org.apache.jackrabbit.spi.QNodeTypeDefinition; +import org.apache.jackrabbit.spi.QPropertyDefinition; +import org.apache.jackrabbit.spi.QValue; +import org.apache.jackrabbit.spi.commons.QNodeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl; +import org.apache.jackrabbit.spi.commons.QPropertyDefinitionImpl; +import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver; +import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl; +import org.apache.jackrabbit.spi.commons.nodetype.InvalidConstraintException; +import org.apache.jackrabbit.spi.commons.nodetype.ValueConstraint; +import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl; +import org.apache.jackrabbit.spi.commons.value.ValueFormat; + +/** + * Default implementations of a {@link QNodeTypeDefinitionsBuilder}. This implementations uses + * {@link QNodeTypeDefinitionBuilderImpl} for building node type definitions, + * {@link QPropertyDefinitionBuilderImpl} for building property definitions, and + * {@link QNodeDefinitionBuilderImpl} for building node definitions. It further uses + * {@link NameFactoryImpl} for creating Names and {@link QValueFactoryImpl} for + * creating QValues. + */ +public class QNodeTypeDefinitionsBuilderImpl extends QNodeTypeDefinitionsBuilder { + + private static final NameFactory NAME_FACTORY = NameFactoryImpl.getInstance(); + + public QNodeTypeDefinitionBuilder newQNodeTypeDefinition() { + return new QNodeTypeDefinitionBuilderImpl(); + } + + public Name createName(String namespaceURI, String localName) { + return NAME_FACTORY.create(namespaceURI, localName); + } + + /** + * Default implementation of a {@link QNodeTypeDefinitionBuilder}. + */ + public class QNodeTypeDefinitionBuilderImpl extends QNodeTypeDefinitionBuilder { + + public QNodeDefinitionBuilder newQNodeDefinitionBuilder() { + return new QNodeDefinitionBuilderImpl(); + } + + public QPropertyDefinitionBuilder newQPropertyDefinition() { + return new QPropertyDefinitionBuilderImpl(); + } + + public QNodeTypeDefinition build() { + return new QNodeTypeDefinitionImpl( + this.getName(), + this.getSuperTypes(), + this.getMixin(), + this.getOrderableChildNodes(), + this.getPrimaryItemName(), + this.getPropertyDefs(), + this.getChildNodeDefs()); + } + + } + + /** + * Default implementation of a {@link QPropertyDefinitionBuilder}. + */ + public class QPropertyDefinitionBuilderImpl extends QPropertyDefinitionBuilder { + + public QValue createValue(String value, NamePathResolver resolver) + throws ValueFormatException, RepositoryException { + + return ValueFormat.getQValue(value, getRequiredType(), resolver, QValueFactoryImpl + .getInstance()); + } + + public String createValueConstraint(String constraint, NamePathResolver resolver) + throws InvalidConstraintException { + + return ValueConstraint.create(getRequiredType(), constraint, resolver).getQualifiedDefinition(); + } + + public QPropertyDefinition build() { + return new QPropertyDefinitionImpl( + this.getName(), + this.getDeclaringNodeType(), + this.getAutoCreated(), + this.getMandatory(), + this.getOnParentVersion(), + this.getProtected(), + this.getDefaultValues(), + this.getMultiple(), + this.getRequiredType(), + this.getValueConstraints()); + } + + } + + /** + * Default implementation of a {@link QNodeDefinitionBuilder}. + */ + public class QNodeDefinitionBuilderImpl extends QNodeDefinitionBuilder { + + public QNodeDefinition build() { + return new QNodeDefinitionImpl( + this.getName(), + this.getDeclaringNodeType(), + this.getAutoCreated(), + this.getMandatory(), + this.getOnParentVersion(), + this.getProtected(), + this.getDefaultPrimaryType(), + this.getRequiredPrimaryTypes(), + this.getAllowsSameNameSiblings()); + } + + } + +}