View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors.
8    *
9    * Unless otherwise indicated, all code in ModeShape is licensed
10   * to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   * 
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.cnd;
25  
26  import static org.modeshape.common.text.TokenStream.ANY_VALUE;
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.Set;
36  import net.jcip.annotations.NotThreadSafe;
37  import org.modeshape.common.collection.Problems;
38  import org.modeshape.common.text.ParsingException;
39  import org.modeshape.common.text.Position;
40  import org.modeshape.common.text.TokenStream;
41  import org.modeshape.common.text.TokenStream.Tokenizer;
42  import org.modeshape.common.util.CheckArg;
43  import org.modeshape.common.util.IoUtil;
44  import org.modeshape.graph.ExecutionContext;
45  import org.modeshape.graph.JcrLexicon;
46  import org.modeshape.graph.JcrNtLexicon;
47  import org.modeshape.graph.io.Destination;
48  import org.modeshape.graph.property.Name;
49  import org.modeshape.graph.property.NameFactory;
50  import org.modeshape.graph.property.Path;
51  import org.modeshape.graph.property.PathFactory;
52  import org.modeshape.graph.property.Property;
53  import org.modeshape.graph.property.PropertyFactory;
54  import org.modeshape.graph.property.PropertyType;
55  import org.modeshape.graph.property.ValueFactories;
56  import org.modeshape.graph.property.ValueFormatException;
57  
58  /**
59   * A class that imports the node types contained in a JCR Compact Node Definition (CND) file into graph content. The content is
60   * written using the graph structured defined by JCR and the "{@code nt:nodeType}", "{@code nt:propertyDefinition}", and "{@code
61   * nt:childNodeDefinition}" node types.
62   * <p>
63   * Although instances of this class never change their behavior and all processing is done in local contexts, {@link Destination}
64   * is not thread-safe and therefore this component may not be considered thread-safe.
65   * </p>
66   */
67  @NotThreadSafe
68  public class CndImporter {
69  
70      protected final List<String> VALID_PROPERTY_TYPES = Collections.unmodifiableList(Arrays.asList(new String[] {"STRING",
71          "BINARY", "LONG", "DOUBLE", "BOOLEAN", "DATE", "NAME", "PATH", "REFERENCE", "WEAKREFERENCE", "DECIMAL", "URI",
72          "UNDEFINED", "*", "?"}));
73  
74      protected final List<String> VALID_ON_PARENT_VERSION = Collections.unmodifiableList(Arrays.asList(new String[] {"COPY",
75          "VERSION", "INITIALIZE", "COMPUTE", "IGNORE", "ABORT"}));
76  
77      protected final Set<String> VALID_QUERY_OPERATORS = Collections.unmodifiableSet(new HashSet<String>(
78                                                                                                          Arrays.asList(new String[] {
79                                                                                                              "=", "<>", "<", "<=",
80                                                                                                              ">", ">=", "LIKE"})));
81  
82      protected final Destination destination;
83      protected final Path outputPath;
84      protected final PropertyFactory propertyFactory;
85      protected final PathFactory pathFactory;
86      protected final NameFactory nameFactory;
87      protected final ValueFactories valueFactories;
88      protected final boolean jcr170;
89  
90      /**
91       * Create a new importer that will place the content in the supplied destination under the supplied path.
92       * 
93       * @param destination the destination where content is to be written
94       * @param parentPath the path in the destination below which the generated content is to appear
95       * @param compatibleWithPreJcr2 true if this parser should accept the CND format that was used in the reference implementation
96       *        prior to JCR 2.0.
97       * @throws IllegalArgumentException if either parameter is null
98       */
99      public CndImporter( Destination destination,
100                         Path parentPath,
101                         boolean compatibleWithPreJcr2 ) {
102         CheckArg.isNotNull(destination, "destination");
103         CheckArg.isNotNull(parentPath, "parentPath");
104         this.destination = destination;
105         this.outputPath = parentPath;
106         ExecutionContext context = destination.getExecutionContext();
107         this.valueFactories = context.getValueFactories();
108         this.propertyFactory = context.getPropertyFactory();
109         this.pathFactory = valueFactories.getPathFactory();
110         this.nameFactory = valueFactories.getNameFactory();
111         this.jcr170 = compatibleWithPreJcr2;
112     }
113 
114     /**
115      * Create a new importer that will place the content in the supplied destination under the supplied path. This parser will
116      * accept the CND format that was used in the reference implementation prior to JCR 2.0.
117      * 
118      * @param destination the destination where content is to be written
119      * @param parentPath the path in the destination below which the generated content is to appear
120      * @throws IllegalArgumentException if either parameter is null
121      */
122     public CndImporter( Destination destination,
123                         Path parentPath ) {
124         this(destination, parentPath, true);
125     }
126 
127     /**
128      * Import the CND content from the supplied stream, placing the content into the importer's destination.
129      * 
130      * @param stream the stream containing the CND content
131      * @param problems where any problems encountered during import should be reported
132      * @param resourceName a logical name for the resource name to be used when reporting problems; may be null if there is no
133      *        useful name
134      * @throws IOException if there is a problem reading from the supplied stream
135      */
136     public void importFrom( InputStream stream,
137                             Problems problems,
138                             String resourceName ) throws IOException {
139         importFrom(IoUtil.read(stream), problems, resourceName);
140     }
141 
142     /**
143      * Import the CND content from the supplied stream, placing the content into the importer's destination.
144      * 
145      * @param file the file containing the CND content
146      * @param problems where any problems encountered during import should be reported
147      * @throws IOException if there is a problem reading from the supplied stream
148      */
149     public void importFrom( File file,
150                             Problems problems ) throws IOException {
151         importFrom(IoUtil.read(file), problems, file.getCanonicalPath());
152     }
153 
154     /**
155      * Import the CND content from the supplied stream, placing the content into the importer's destination.
156      * 
157      * @param content the string containing the CND content
158      * @param problems where any problems encountered during import should be reported
159      * @param resourceName a logical name for the resource name to be used when reporting problems; may be null if there is no
160      *        useful name
161      */
162     public void importFrom( String content,
163                             Problems problems,
164                             String resourceName ) {
165         try {
166             parse(content);
167             destination.submit();
168         } catch (RuntimeException e) {
169             problems.addError(e, CndI18n.errorImportingCndContent, (Object)resourceName, e.getMessage());
170         }
171     }
172 
173     /**
174      * Parse the CND content.
175      * 
176      * @param content the content
177      * @throws ParsingException if there is a problem parsing the content
178      */
179     protected void parse( String content ) {
180         Tokenizer tokenizer = new CndTokenizer(false, false);
181         TokenStream tokens = new TokenStream(content, tokenizer, false);
182         tokens.start();
183         while (tokens.hasNext()) {
184             // Keep reading while we can recognize one of the two types of statements ...
185             if (tokens.matches("<", ANY_VALUE, "=", ANY_VALUE, ">")) {
186                 parseNamespaceMapping(tokens);
187             } else if (tokens.matches("[", ANY_VALUE, "]")) {
188                 parseNodeTypeDefinition(tokens, outputPath);
189             } else {
190                 Position position = tokens.previousPosition();
191                 throw new ParsingException(position, CndI18n.expectedNamespaceOrNodeDefinition.text(tokens.consume(),
192                                                                                                     position.getLine(),
193                                                                                                     position.getColumn()));
194             }
195         }
196     }
197 
198     /**
199      * Parse the namespace mapping statement that is next on the token stream.
200      * 
201      * @param tokens the tokens containing the namespace statement; never null
202      * @throws ParsingException if there is a problem parsing the content
203      */
204     protected void parseNamespaceMapping( TokenStream tokens ) {
205         tokens.consume('<');
206         String prefix = removeQuotes(tokens.consume());
207         tokens.consume('=');
208         String uri = removeQuotes(tokens.consume());
209         tokens.consume('>');
210         // Register the namespace ...
211         destination.getExecutionContext().getNamespaceRegistry().register(prefix, uri);
212     }
213 
214     /**
215      * Parse the node type definition that is next on the token stream.
216      * 
217      * @param tokens the tokens containing the node type definition; never null
218      * @param path the path in the destination under which the node type definition should be stored; never null
219      * @throws ParsingException if there is a problem parsing the content
220      */
221     protected void parseNodeTypeDefinition( TokenStream tokens,
222                                             Path path ) {
223         // Parse the name, and create the path and a property for the name ...
224         Name name = parseNodeTypeName(tokens);
225         Path nodeTypePath = pathFactory.create(path, name);
226         List<Property> properties = new ArrayList<Property>();
227         properties.add(propertyFactory.create(JcrLexicon.NODE_TYPE_NAME, name));
228         properties.add(propertyFactory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.NODE_TYPE));
229 
230         // Read the (optional) supertypes ...
231         List<Name> supertypes = parseSupertypes(tokens);
232         properties.add(propertyFactory.create(JcrLexicon.SUPERTYPES, supertypes)); // even if empty
233 
234         // Read the node type options ...
235         parseNodeTypeOptions(tokens, properties);
236         destination.create(nodeTypePath, properties);
237 
238         // Parse property and child node definitions ...
239         parsePropertyOrChildNodeDefinitions(tokens, nodeTypePath);
240     }
241 
242     /**
243      * Parse a node type name that appears next on the token stream.
244      * 
245      * @param tokens the tokens containing the node type name; never null
246      * @return the node type name
247      * @throws ParsingException if there is a problem parsing the content
248      */
249     protected Name parseNodeTypeName( TokenStream tokens ) {
250         tokens.consume('[');
251         Name name = parseName(tokens);
252         tokens.consume(']');
253         return name;
254     }
255 
256     /**
257      * Parse an optional list of supertypes if they appear next on the token stream.
258      * 
259      * @param tokens the tokens containing the supertype names; never null
260      * @return the list of supertype names; never null, but possibly empty
261      * @throws ParsingException if there is a problem parsing the content
262      */
263     protected List<Name> parseSupertypes( TokenStream tokens ) {
264         if (tokens.canConsume('>')) {
265             // There is at least one supertype ...
266             return parseNameList(tokens);
267         }
268         return Collections.emptyList();
269     }
270 
271     /**
272      * Parse a list of strings, separated by commas. Any quotes surrounding the strings are removed.
273      * 
274      * @param tokens the tokens containing the comma-separated strings; never null
275      * @return the list of string values; never null, but possibly empty
276      * @throws ParsingException if there is a problem parsing the content
277      */
278     protected List<String> parseStringList( TokenStream tokens ) {
279         List<String> strings = new ArrayList<String>();
280         if (tokens.canConsume('?')) {
281             // This list is variant ...
282             strings.add("?");
283         } else {
284             // Read names until we see a ','
285             do {
286                 strings.add(removeQuotes(tokens.consume()));
287             } while (tokens.canConsume(','));
288         }
289         return strings;
290     }
291 
292     /**
293      * Parse a list of names, separated by commas. Any quotes surrounding the names are removed.
294      * 
295      * @param tokens the tokens containing the comma-separated strings; never null
296      * @return the list of string values; never null, but possibly empty
297      * @throws ParsingException if there is a problem parsing the content
298      */
299     protected List<Name> parseNameList( TokenStream tokens ) {
300         List<Name> names = new ArrayList<Name>();
301         if (!tokens.canConsume('?')) {
302             // Read names until we see a ','
303             do {
304                 names.add(parseName(tokens));
305             } while (tokens.canConsume(','));
306         }
307         return names;
308     }
309 
310     /**
311      * Parse the options for the node types, including whether the node type is orderable, a mixin, abstract, whether it supports
312      * querying, and which property/child node (if any) is the primary item for the node type.
313      * 
314      * @param tokens the tokens containing the comma-separated strings; never null
315      * @param properties the list into which the properties that represent the options should be placed
316      * @throws ParsingException if there is a problem parsing the content
317      */
318     protected void parseNodeTypeOptions( TokenStream tokens,
319                                          List<Property> properties ) {
320         // Set up the defaults ...
321         boolean isOrderable = false;
322         boolean isMixin = false;
323         boolean isAbstract = false;
324         boolean isQueryable = true;
325         Name primaryItem = null;
326         String onParentVersion = "COPY";
327         while (true) {
328             // Keep reading while we see a valid option ...
329             if (tokens.canConsumeAnyOf("ORDERABLE", "ORD", "O")) {
330                 tokens.canConsume('?');
331                 isOrderable = true;
332             } else if (tokens.canConsumeAnyOf("MIXIN", "MIX", "M")) {
333                 tokens.canConsume('?');
334                 isMixin = true;
335             } else if (tokens.canConsumeAnyOf("ABSTRACT", "ABS", "A")) {
336                 tokens.canConsume('?');
337                 isAbstract = true;
338             } else if (tokens.canConsumeAnyOf("NOQUERY", "NOQ")) {
339                 tokens.canConsume('?');
340                 isQueryable = false;
341             } else if (tokens.canConsumeAnyOf("QUERY", "Q")) {
342                 tokens.canConsume('?');
343                 isQueryable = true;
344             } else if (tokens.canConsumeAnyOf("PRIMARYITEM", "!")) {
345                 primaryItem = parseName(tokens);
346                 tokens.canConsume('?');
347             } else if (tokens.matchesAnyOf(VALID_ON_PARENT_VERSION)) {
348                 onParentVersion = tokens.consume();
349                 tokens.canConsume('?');
350             } else if (tokens.matches("OPV")) {
351                 // variant on-parent-version
352                 onParentVersion = tokens.consume();
353                 tokens.canConsume('?');
354             } else {
355                 // No more valid options on the stream, so stop ...
356                 break;
357             }
358         }
359         properties.add(propertyFactory.create(JcrLexicon.HAS_ORDERABLE_CHILD_NODES, isOrderable));
360         properties.add(propertyFactory.create(JcrLexicon.IS_MIXIN, isMixin));
361         properties.add(propertyFactory.create(JcrLexicon.IS_ABSTRACT, isAbstract));
362         properties.add(propertyFactory.create(JcrLexicon.IS_QUERYABLE, isQueryable));
363         properties.add(propertyFactory.create(JcrLexicon.ON_PARENT_VERSION, onParentVersion.toUpperCase()));
364         if (primaryItem != null) {
365             properties.add(propertyFactory.create(JcrLexicon.PRIMARY_ITEM_NAME, primaryItem));
366         }
367     }
368 
369     /**
370      * Parse a node type's property or child node definitions that appear next on the token stream.
371      * 
372      * @param tokens the tokens containing the definitions; never null
373      * @param nodeTypePath the path in the destination where the node type has been created, and under which the property and
374      *        child node type definitions should be placed
375      * @throws ParsingException if there is a problem parsing the content
376      */
377     protected void parsePropertyOrChildNodeDefinitions( TokenStream tokens,
378                                                         Path nodeTypePath ) {
379         while (true) {
380             // Keep reading while we see a property definition or child node definition ...
381             if (tokens.matches('-')) {
382                 parsePropertyDefinition(tokens, nodeTypePath);
383             } else if (tokens.matches('+')) {
384                 parseChildNodeDefinition(tokens, nodeTypePath);
385             } else {
386                 // The next token does not signal either one of these, so stop ...
387                 break;
388             }
389         }
390     }
391 
392     /**
393      * Parse a node type's property definition from the next tokens on the stream.
394      * 
395      * @param tokens the tokens containing the definition; never null
396      * @param nodeTypePath the path in the destination where the node type has been created, and under which the property and
397      *        child node type definitions should be placed
398      * @throws ParsingException if there is a problem parsing the content
399      */
400     protected void parsePropertyDefinition( TokenStream tokens,
401                                             Path nodeTypePath ) {
402         tokens.consume('-');
403         Name name = parseName(tokens);
404         Path path = pathFactory.create(nodeTypePath, JcrLexicon.PROPERTY_DEFINITION);
405         List<Property> properties = new ArrayList<Property>();
406         properties.add(propertyFactory.create(JcrLexicon.NAME, name));
407         properties.add(propertyFactory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.PROPERTY_DEFINITION));
408 
409         // Parse the (optional) required type ...
410         parsePropertyType(tokens, properties, PropertyType.STRING.getName());
411 
412         // Parse the default values ...
413         parseDefaultValues(tokens, properties);
414 
415         // Parse the property attributes ...
416         parsePropertyAttributes(tokens, properties, name, path);
417 
418         // Parse the property constraints ...
419         parseValueConstraints(tokens, properties);
420 
421         // Create the node in the destination ...
422         destination.create(path, properties);
423     }
424 
425     /**
426      * Parse the property type, if a valid one appears next on the token stream.
427      * 
428      * @param tokens the tokens containing the definition; never null
429      * @param properties the list into which the property that represents the property type should be placed
430      * @param defaultPropertyType the default property type if none is actually found
431      * @throws ParsingException if there is a problem parsing the content
432      */
433     protected void parsePropertyType( TokenStream tokens,
434                                       List<Property> properties,
435                                       String defaultPropertyType ) {
436         if (tokens.canConsume('(')) {
437             // Parse the (optional) property type ...
438             String propertyType = defaultPropertyType;
439             if (tokens.matchesAnyOf(VALID_PROPERTY_TYPES)) {
440                 propertyType = tokens.consume();
441                 if ("*".equals(propertyType)) propertyType = "UNDEFINED";
442             }
443             tokens.consume(')');
444             properties.add(propertyFactory.create(JcrLexicon.REQUIRED_TYPE, propertyType.toUpperCase()));
445         }
446     }
447 
448     /**
449      * Parse the property definition's default value, if they appear next on the token stream.
450      * 
451      * @param tokens the tokens containing the definition; never null
452      * @param properties the list into which the property that represents the default values should be placed
453      * @throws ParsingException if there is a problem parsing the content
454      */
455     protected void parseDefaultValues( TokenStream tokens,
456                                        List<Property> properties ) {
457         if (tokens.canConsume('=')) {
458             List<String> defaultValues = parseStringList(tokens);
459             if (!defaultValues.isEmpty()) {
460                 properties.add(propertyFactory.create(JcrLexicon.DEFAULT_VALUES, defaultValues));
461             }
462         }
463     }
464 
465     /**
466      * Parse the property definition's value constraints, if they appear next on the token stream.
467      * 
468      * @param tokens the tokens containing the definition; never null
469      * @param properties the list into which the property that represents the value constraints should be placed
470      * @throws ParsingException if there is a problem parsing the content
471      */
472     protected void parseValueConstraints( TokenStream tokens,
473                                           List<Property> properties ) {
474         if (tokens.canConsume('<')) {
475             List<String> defaultValues = parseStringList(tokens);
476             if (!defaultValues.isEmpty()) {
477                 properties.add(propertyFactory.create(JcrLexicon.VALUE_CONSTRAINTS, defaultValues));
478             }
479         }
480     }
481 
482     /**
483      * Parse the property definition's attributes, if they appear next on the token stream.
484      * 
485      * @param tokens the tokens containing the attributes; never null
486      * @param properties the list into which the properties that represents the attributes should be placed
487      * @param propDefnName the name of the property definition; never null
488      * @param propDefnPath the path in the destination to the property definition node; never null
489      * @throws ParsingException if there is a problem parsing the content
490      */
491     protected void parsePropertyAttributes( TokenStream tokens,
492                                             List<Property> properties,
493                                             Name propDefnName,
494                                             Path propDefnPath ) {
495         boolean autoCreated = false;
496         boolean mandatory = false;
497         boolean isProtected = false;
498         boolean multiple = false;
499         boolean isFullTextSearchable = true;
500         boolean isQueryOrderable = true;
501         String onParentVersion = "COPY";
502         while (true) {
503             if (tokens.canConsumeAnyOf("AUTOCREATED", "AUT", "A")) {
504                 tokens.canConsume('?');
505                 autoCreated = true;
506             } else if (tokens.canConsumeAnyOf("MANDATORY", "MAN", "M")) {
507                 tokens.canConsume('?');
508                 mandatory = true;
509             } else if (tokens.canConsumeAnyOf("PROTECTED", "PRO", "P")) {
510                 tokens.canConsume('?');
511                 isProtected = true;
512             } else if (tokens.canConsumeAnyOf("MULTIPLE", "MUL", "*")) {
513                 tokens.canConsume('?');
514                 multiple = true;
515             } else if (tokens.matchesAnyOf(VALID_ON_PARENT_VERSION)) {
516                 onParentVersion = tokens.consume();
517                 tokens.canConsume('?');
518             } else if (tokens.matches("OPV")) {
519                 // variant on-parent-version
520                 onParentVersion = tokens.consume();
521                 tokens.canConsume('?');
522             } else if (tokens.canConsumeAnyOf("NOFULLTEXT", "NOF")) {
523                 tokens.canConsume('?');
524                 isFullTextSearchable = false;
525             } else if (tokens.canConsumeAnyOf("NOQUERYORDER", "NQORD")) {
526                 tokens.canConsume('?');
527                 isQueryOrderable = false;
528             } else if (tokens.canConsumeAnyOf("QUERYOPS", "QOP")) {
529                 parseQueryOperators(tokens, properties);
530             } else if (tokens.canConsumeAnyOf("PRIMARY", "PRI", "!")) {
531                 if (!jcr170) {
532                     Position pos = tokens.previousPosition();
533                     int line = pos.getLine();
534                     int column = pos.getColumn();
535                     throw new ParsingException(tokens.previousPosition(),
536                                                CndI18n.primaryKeywordNotValidInJcr2CndFormat.text(line, column));
537                 }
538                 // Then this child node is considered the primary item ...
539                 Property primaryItem = propertyFactory.create(JcrLexicon.PRIMARY_ITEM_NAME, propDefnName);
540                 destination.setProperties(propDefnPath.getParent(), primaryItem);
541             } else {
542                 break;
543             }
544         }
545         properties.add(propertyFactory.create(JcrLexicon.AUTO_CREATED, autoCreated));
546         properties.add(propertyFactory.create(JcrLexicon.MANDATORY, mandatory));
547         properties.add(propertyFactory.create(JcrLexicon.PROTECTED, isProtected));
548         properties.add(propertyFactory.create(JcrLexicon.ON_PARENT_VERSION, onParentVersion.toUpperCase()));
549         properties.add(propertyFactory.create(JcrLexicon.MULTIPLE, multiple));
550         properties.add(propertyFactory.create(JcrLexicon.IS_FULL_TEXT_SEARCHABLE, isFullTextSearchable));
551         properties.add(propertyFactory.create(JcrLexicon.IS_QUERY_ORDERABLE, isQueryOrderable));
552     }
553 
554     /**
555      * Parse the property definition's query operators, if they appear next on the token stream.
556      * 
557      * @param tokens the tokens containing the definition; never null
558      * @param properties the list into which the property that represents the value constraints should be placed
559      * @throws ParsingException if there is a problem parsing the content
560      */
561     protected void parseQueryOperators( TokenStream tokens,
562                                         List<Property> properties ) {
563         if (tokens.canConsume('?')) {
564             return;
565         }
566         // The query operators are expected to be enclosed in a single quote, so therefore will be a single token ...
567         List<String> operators = new ArrayList<String>();
568         String operatorList = removeQuotes(tokens.consume());
569         // Now split this string on ',' ...
570         for (String operatorValue : operatorList.split(",")) {
571             String operator = operatorValue.trim();
572             if (!VALID_QUERY_OPERATORS.contains(operator)) {
573                 throw new ParsingException(tokens.previousPosition(), CndI18n.expectedValidQueryOperator.text(operator));
574             }
575             operators.add(operator);
576         }
577         if (operators.isEmpty()) {
578             operators.addAll(VALID_QUERY_OPERATORS);
579         }
580         properties.add(propertyFactory.create(JcrLexicon.QUERY_OPERATORS, operators));
581     }
582 
583     /**
584      * Parse a node type's child node definition from the next tokens on the stream.
585      * 
586      * @param tokens the tokens containing the definition; never null
587      * @param nodeTypePath the path in the destination where the node type has been created, and under which the child node type
588      *        definitions should be placed
589      * @throws ParsingException if there is a problem parsing the content
590      */
591     protected void parseChildNodeDefinition( TokenStream tokens,
592                                              Path nodeTypePath ) {
593         tokens.consume('+');
594         Name name = parseName(tokens);
595         Path path = pathFactory.create(nodeTypePath, JcrLexicon.CHILD_NODE_DEFINITION);
596         List<Property> properties = new ArrayList<Property>();
597         properties.add(propertyFactory.create(JcrLexicon.NAME, name));
598         properties.add(propertyFactory.create(JcrLexicon.PRIMARY_TYPE, JcrNtLexicon.CHILD_NODE_DEFINITION));
599 
600         parseRequiredPrimaryTypes(tokens, properties);
601         parseDefaultType(tokens, properties);
602         parseNodeAttributes(tokens, properties, name, path);
603 
604         // Create the node in the destination ...
605         destination.create(path, properties);
606     }
607 
608     /**
609      * Parse the child node definition's list of required primary types, if they appear next on the token stream.
610      * 
611      * @param tokens the tokens containing the definition; never null
612      * @param properties the list into which the property that represents the required types should be placed
613      * @throws ParsingException if there is a problem parsing the content
614      */
615     protected void parseRequiredPrimaryTypes( TokenStream tokens,
616                                               List<Property> properties ) {
617         if (tokens.canConsume('(')) {
618             List<Name> requiredTypes = parseNameList(tokens);
619             if (requiredTypes.isEmpty()) {
620                 requiredTypes.add(JcrNtLexicon.BASE);
621             }
622             properties.add(propertyFactory.create(JcrLexicon.REQUIRED_PRIMARY_TYPES, requiredTypes));
623             tokens.consume(')');
624         }
625     }
626 
627     /**
628      * Parse the child node definition's default type, if they appear next on the token stream.
629      * 
630      * @param tokens the tokens containing the definition; never null
631      * @param properties the list into which the property that represents the default primary type should be placed
632      * @throws ParsingException if there is a problem parsing the content
633      */
634     protected void parseDefaultType( TokenStream tokens,
635                                      List<Property> properties ) {
636         if (tokens.canConsume('=')) {
637             if (!tokens.canConsume('?')) {
638                 Name defaultType = parseName(tokens);
639                 properties.add(propertyFactory.create(JcrLexicon.DEFAULT_PRIMARY_TYPE, defaultType));
640             }
641         }
642     }
643 
644     /**
645      * Parse the child node definition's attributes, if they appear next on the token stream.
646      * 
647      * @param tokens the tokens containing the attributes; never null
648      * @param properties the list into which the properties that represents the attributes should be placed
649      * @param childNodeDefnName the name of the child node definition; never null
650      * @param childNodeDefnPath the path in the destination to the child node definition node; never null
651      * @throws ParsingException if there is a problem parsing the content
652      */
653     protected void parseNodeAttributes( TokenStream tokens,
654                                         List<Property> properties,
655                                         Name childNodeDefnName,
656                                         Path childNodeDefnPath ) {
657         boolean autoCreated = false;
658         boolean mandatory = false;
659         boolean isProtected = false;
660         boolean sns = false;
661         String onParentVersion = "COPY";
662         while (true) {
663             if (tokens.canConsumeAnyOf("AUTOCREATED", "AUT", "A")) {
664                 tokens.canConsume('?');
665                 autoCreated = true;
666             } else if (tokens.canConsumeAnyOf("MANDATORY", "MAN", "M")) {
667                 tokens.canConsume('?');
668                 mandatory = true;
669             } else if (tokens.canConsumeAnyOf("PROTECTED", "PRO", "P")) {
670                 tokens.canConsume('?');
671                 isProtected = true;
672             } else if (tokens.canConsumeAnyOf("SNS", "*")) { // standard JCR 2.0 keywords for SNS ...
673                 tokens.canConsume('?');
674                 sns = true;
675             } else if (tokens.canConsumeAnyOf("MULTIPLE", "MUL", "*")) { // from pre-JCR 2.0 ref impl
676                 if (!jcr170) {
677                     Position pos = tokens.previousPosition();
678                     int line = pos.getLine();
679                     int column = pos.getColumn();
680                     throw new ParsingException(tokens.previousPosition(),
681                                                CndI18n.multipleKeywordNotValidInJcr2CndFormat.text(line, column));
682                 }
683                 tokens.canConsume('?');
684                 sns = true;
685             } else if (tokens.matchesAnyOf(VALID_ON_PARENT_VERSION)) {
686                 onParentVersion = tokens.consume();
687                 tokens.canConsume('?');
688             } else if (tokens.matches("OPV")) {
689                 // variant on-parent-version
690                 onParentVersion = tokens.consume();
691                 tokens.canConsume('?');
692             } else if (tokens.canConsumeAnyOf("PRIMARYITEM", "PRIMARY", "PRI", "!")) {
693                 // Then this child node is considered the primary item ...
694                 Property primaryItem = propertyFactory.create(JcrLexicon.PRIMARY_ITEM_NAME, childNodeDefnName);
695                 destination.setProperties(childNodeDefnPath.getParent(), primaryItem);
696             } else {
697                 break;
698             }
699         }
700         properties.add(propertyFactory.create(JcrLexicon.AUTO_CREATED, autoCreated));
701         properties.add(propertyFactory.create(JcrLexicon.MANDATORY, mandatory));
702         properties.add(propertyFactory.create(JcrLexicon.PROTECTED, isProtected));
703         properties.add(propertyFactory.create(JcrLexicon.ON_PARENT_VERSION, onParentVersion.toUpperCase()));
704         properties.add(propertyFactory.create(JcrLexicon.SAME_NAME_SIBLINGS, sns));
705     }
706 
707     /**
708      * Parse the name that is expected to be next on the token stream.
709      * 
710      * @param tokens the tokens containing the name; never null
711      * @return the name; never null
712      * @throws ParsingException if there is a problem parsing the content
713      */
714     protected Name parseName( TokenStream tokens ) {
715         String value = tokens.consume();
716         try {
717             return nameFactory.create(removeQuotes(value));
718         } catch (ValueFormatException e) {
719             throw new ParsingException(tokens.previousPosition(), CndI18n.expectedValidNameLiteral.text(value));
720         }
721     }
722 
723     protected final String removeQuotes( String text ) {
724         // Remove leading and trailing quotes, if there are any ...
725         return text.replaceFirst("^['\"]+", "").replaceAll("['\"]+$", "");
726     }
727 }