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 
229         // Read the (optional) supertypes ...
230         List<Name> supertypes = parseSupertypes(tokens);
231         properties.add(propertyFactory.create(JcrLexicon.SUPERTYPES, supertypes)); // even if empty
232 
233         // Read the node type options ...
234         parseNodeTypeOptions(tokens, properties);
235         destination.create(nodeTypePath, properties);
236 
237         // Parse property and child node definitions ...
238         parsePropertyOrChildNodeDefinitions(tokens, nodeTypePath);
239     }
240 
241     /**
242      * Parse a node type name that appears next on the token stream.
243      * 
244      * @param tokens the tokens containing the node type name; never null
245      * @return the node type name
246      * @throws ParsingException if there is a problem parsing the content
247      */
248     protected Name parseNodeTypeName( TokenStream tokens ) {
249         tokens.consume('[');
250         Name name = parseName(tokens);
251         tokens.consume(']');
252         return name;
253     }
254 
255     /**
256      * Parse an optional list of supertypes if they appear next on the token stream.
257      * 
258      * @param tokens the tokens containing the supertype names; never null
259      * @return the list of supertype names; never null, but possibly empty
260      * @throws ParsingException if there is a problem parsing the content
261      */
262     protected List<Name> parseSupertypes( TokenStream tokens ) {
263         if (tokens.canConsume('>')) {
264             // There is at least one supertype ...
265             return parseNameList(tokens);
266         }
267         return Collections.emptyList();
268     }
269 
270     /**
271      * Parse a list of strings, separated by commas. Any quotes surrounding the strings are removed.
272      * 
273      * @param tokens the tokens containing the comma-separated strings; never null
274      * @return the list of string values; never null, but possibly empty
275      * @throws ParsingException if there is a problem parsing the content
276      */
277     protected List<String> parseStringList( TokenStream tokens ) {
278         List<String> strings = new ArrayList<String>();
279         if (tokens.canConsume('?')) {
280             // This list is variant ...
281             strings.add("?");
282         } else {
283             // Read names until we see a ','
284             do {
285                 strings.add(removeQuotes(tokens.consume()));
286             } while (tokens.canConsume(','));
287         }
288         return strings;
289     }
290 
291     /**
292      * Parse a list of names, separated by commas. Any quotes surrounding the names are removed.
293      * 
294      * @param tokens the tokens containing the comma-separated strings; never null
295      * @return the list of string values; never null, but possibly empty
296      * @throws ParsingException if there is a problem parsing the content
297      */
298     protected List<Name> parseNameList( TokenStream tokens ) {
299         List<Name> names = new ArrayList<Name>();
300         if (!tokens.canConsume('?')) {
301             // Read names until we see a ','
302             do {
303                 names.add(parseName(tokens));
304             } while (tokens.canConsume(','));
305         }
306         return names;
307     }
308 
309     /**
310      * Parse the options for the node types, including whether the node type is orderable, a mixin, abstract, whether it supports
311      * querying, and which property/child node (if any) is the primary item for the node type.
312      * 
313      * @param tokens the tokens containing the comma-separated strings; never null
314      * @param properties the list into which the properties that represent the options should be placed
315      * @throws ParsingException if there is a problem parsing the content
316      */
317     protected void parseNodeTypeOptions( TokenStream tokens,
318                                          List<Property> properties ) {
319         // Set up the defaults ...
320         boolean isOrderable = false;
321         boolean isMixin = false;
322         boolean isAbstract = false;
323         boolean isQueryable = true;
324         Name primaryItem = null;
325         String onParentVersion = "COPY";
326         while (true) {
327             // Keep reading while we see a valid option ...
328             if (tokens.canConsumeAnyOf("ORDERABLE", "ORD", "O")) {
329                 tokens.canConsume('?');
330                 isOrderable = true;
331             } else if (tokens.canConsumeAnyOf("MIXIN", "MIX", "M")) {
332                 tokens.canConsume('?');
333                 isMixin = true;
334             } else if (tokens.canConsumeAnyOf("ABSTRACT", "ABS", "A")) {
335                 tokens.canConsume('?');
336                 isAbstract = true;
337             } else if (tokens.canConsumeAnyOf("NOQUERY", "NOQ")) {
338                 tokens.canConsume('?');
339                 isQueryable = false;
340             } else if (tokens.canConsumeAnyOf("PRIMARYITEM", "!")) {
341                 primaryItem = parseName(tokens);
342                 tokens.canConsume('?');
343             } else if (tokens.matchesAnyOf(VALID_ON_PARENT_VERSION)) {
344                 onParentVersion = tokens.consume();
345                 tokens.canConsume('?');
346             } else if (tokens.matches("OPV")) {
347                 // variant on-parent-version
348                 onParentVersion = tokens.consume();
349                 tokens.canConsume('?');
350             } else {
351                 // No more valid options on the stream, so stop ...
352                 break;
353             }
354         }
355         properties.add(propertyFactory.create(JcrLexicon.HAS_ORDERABLE_CHILD_NODES, isOrderable));
356         properties.add(propertyFactory.create(JcrLexicon.IS_MIXIN, isMixin));
357         properties.add(propertyFactory.create(JcrLexicon.IS_ABSTRACT, isAbstract));
358         properties.add(propertyFactory.create(JcrLexicon.IS_QUERYABLE, isQueryable));
359         properties.add(propertyFactory.create(JcrLexicon.ON_PARENT_VERSION, onParentVersion.toUpperCase()));
360         if (primaryItem != null) {
361             properties.add(propertyFactory.create(JcrLexicon.PRIMARY_ITEM_NAME, primaryItem));
362         }
363     }
364 
365     /**
366      * Parse a node type's property or child node definitions that appear next on the token stream.
367      * 
368      * @param tokens the tokens containing the definitions; never null
369      * @param nodeTypePath the path in the destination where the node type has been created, and under which the property and
370      *        child node type definitions should be placed
371      * @throws ParsingException if there is a problem parsing the content
372      */
373     protected void parsePropertyOrChildNodeDefinitions( TokenStream tokens,
374                                                         Path nodeTypePath ) {
375         while (true) {
376             // Keep reading while we see a property definition or child node definition ...
377             if (tokens.matches('-')) {
378                 parsePropertyDefinition(tokens, nodeTypePath);
379             } else if (tokens.matches('+')) {
380                 parseChildNodeDefinition(tokens, nodeTypePath);
381             } else {
382                 // The next token does not signal either one of these, so stop ...
383                 break;
384             }
385         }
386     }
387 
388     /**
389      * Parse a node type's property definition from the next tokens on the stream.
390      * 
391      * @param tokens the tokens containing the definition; never null
392      * @param nodeTypePath the path in the destination where the node type has been created, and under which the property and
393      *        child node type definitions should be placed
394      * @throws ParsingException if there is a problem parsing the content
395      */
396     protected void parsePropertyDefinition( TokenStream tokens,
397                                             Path nodeTypePath ) {
398         tokens.consume('-');
399         Name name = parseName(tokens);
400         Path path = pathFactory.create(nodeTypePath, JcrLexicon.PROPERTY_DEFINITION);
401         List<Property> properties = new ArrayList<Property>();
402         properties.add(propertyFactory.create(JcrLexicon.NAME, name));
403 
404         // Parse the (optional) required type ...
405         parsePropertyType(tokens, properties, PropertyType.STRING.getName());
406 
407         // Parse the default values ...
408         parseDefaultValues(tokens, properties);
409 
410         // Parse the property attributes ...
411         parsePropertyAttributes(tokens, properties, name, path);
412 
413         // Parse the property constraints ...
414         parseValueConstraints(tokens, properties);
415 
416         // Create the node in the destination ...
417         destination.create(path, properties);
418     }
419 
420     /**
421      * Parse the property type, if a valid one appears next on the token stream.
422      * 
423      * @param tokens the tokens containing the definition; never null
424      * @param properties the list into which the property that represents the property type should be placed
425      * @param defaultPropertyType the default property type if none is actually found
426      * @throws ParsingException if there is a problem parsing the content
427      */
428     protected void parsePropertyType( TokenStream tokens,
429                                       List<Property> properties,
430                                       String defaultPropertyType ) {
431         if (tokens.canConsume('(')) {
432             // Parse the (optional) property type ...
433             String propertyType = defaultPropertyType;
434             if (tokens.matchesAnyOf(VALID_PROPERTY_TYPES)) {
435                 propertyType = tokens.consume();
436                 if ("*".equals(propertyType)) propertyType = "UNDEFINED";
437             }
438             tokens.consume(')');
439             properties.add(propertyFactory.create(JcrLexicon.REQUIRED_TYPE, propertyType.toUpperCase()));
440         }
441     }
442 
443     /**
444      * Parse the property definition's default value, if they appear next on the token stream.
445      * 
446      * @param tokens the tokens containing the definition; never null
447      * @param properties the list into which the property that represents the default values should be placed
448      * @throws ParsingException if there is a problem parsing the content
449      */
450     protected void parseDefaultValues( TokenStream tokens,
451                                        List<Property> properties ) {
452         if (tokens.canConsume('=')) {
453             List<String> defaultValues = parseStringList(tokens);
454             if (!defaultValues.isEmpty()) {
455                 properties.add(propertyFactory.create(JcrLexicon.DEFAULT_VALUES, defaultValues));
456             }
457         }
458     }
459 
460     /**
461      * Parse the property definition's value constraints, if they appear next on the token stream.
462      * 
463      * @param tokens the tokens containing the definition; never null
464      * @param properties the list into which the property that represents the value constraints should be placed
465      * @throws ParsingException if there is a problem parsing the content
466      */
467     protected void parseValueConstraints( TokenStream tokens,
468                                           List<Property> properties ) {
469         if (tokens.canConsume('<')) {
470             List<String> defaultValues = parseStringList(tokens);
471             if (!defaultValues.isEmpty()) {
472                 properties.add(propertyFactory.create(JcrLexicon.VALUE_CONSTRAINTS, defaultValues));
473             }
474         }
475     }
476 
477     /**
478      * Parse the property definition's attributes, if they appear next on the token stream.
479      * 
480      * @param tokens the tokens containing the attributes; never null
481      * @param properties the list into which the properties that represents the attributes should be placed
482      * @param propDefnName the name of the property definition; never null
483      * @param propDefnPath the path in the destination to the property definition node; never null
484      * @throws ParsingException if there is a problem parsing the content
485      */
486     protected void parsePropertyAttributes( TokenStream tokens,
487                                             List<Property> properties,
488                                             Name propDefnName,
489                                             Path propDefnPath ) {
490         boolean autoCreated = false;
491         boolean mandatory = false;
492         boolean isProtected = false;
493         boolean multiple = false;
494         boolean isFullTextSearchable = true;
495         boolean isQueryOrderable = true;
496         String onParentVersion = "COPY";
497         while (true) {
498             if (tokens.canConsumeAnyOf("AUTOCREATED", "AUT", "A")) {
499                 tokens.canConsume('?');
500                 autoCreated = true;
501             } else if (tokens.canConsumeAnyOf("MANDATORY", "MAN", "M")) {
502                 tokens.canConsume('?');
503                 mandatory = true;
504             } else if (tokens.canConsumeAnyOf("PROTECTED", "PRO", "P")) {
505                 tokens.canConsume('?');
506                 isProtected = true;
507             } else if (tokens.canConsumeAnyOf("MULTIPLE", "MUL", "*")) {
508                 tokens.canConsume('?');
509                 multiple = true;
510             } else if (tokens.matchesAnyOf(VALID_ON_PARENT_VERSION)) {
511                 onParentVersion = tokens.consume();
512                 tokens.canConsume('?');
513             } else if (tokens.matches("OPV")) {
514                 // variant on-parent-version
515                 onParentVersion = tokens.consume();
516                 tokens.canConsume('?');
517             } else if (tokens.canConsumeAnyOf("NOFULLTEXT", "NOF")) {
518                 tokens.canConsume('?');
519                 isFullTextSearchable = false;
520             } else if (tokens.canConsumeAnyOf("NOQUERYORDER", "NQORD")) {
521                 tokens.canConsume('?');
522                 isQueryOrderable = false;
523             } else if (tokens.canConsumeAnyOf("QUERYOPS", "QOP")) {
524                 parseQueryOperators(tokens, properties);
525             } else if (jcr170 && tokens.canConsumeAnyOf("PRIMARY", "PRI", "!")) {
526                 // Then this child node is considered the primary item ...
527                 Property primaryItem = propertyFactory.create(JcrLexicon.PRIMARY_ITEM_NAME, propDefnName);
528                 destination.setProperties(propDefnPath.getParent(), primaryItem);
529             } else {
530                 break;
531             }
532         }
533         properties.add(propertyFactory.create(JcrLexicon.AUTO_CREATED, autoCreated));
534         properties.add(propertyFactory.create(JcrLexicon.MANDATORY, mandatory));
535         properties.add(propertyFactory.create(JcrLexicon.PROTECTED, isProtected));
536         properties.add(propertyFactory.create(JcrLexicon.ON_PARENT_VERSION, onParentVersion.toUpperCase()));
537         properties.add(propertyFactory.create(JcrLexicon.MULTIPLE, multiple));
538         properties.add(propertyFactory.create(JcrLexicon.IS_FULL_TEXT_SEARCHABLE, isFullTextSearchable));
539         properties.add(propertyFactory.create(JcrLexicon.IS_QUERY_ORDERABLE, isQueryOrderable));
540     }
541 
542     /**
543      * Parse the property definition's query operators, if they appear next on the token stream.
544      * 
545      * @param tokens the tokens containing the definition; never null
546      * @param properties the list into which the property that represents the value constraints should be placed
547      * @throws ParsingException if there is a problem parsing the content
548      */
549     protected void parseQueryOperators( TokenStream tokens,
550                                         List<Property> properties ) {
551         if (tokens.canConsume('?')) {
552             return;
553         }
554         // The query operators are expected to be enclosed in a single quote, so therefore will be a single token ...
555         List<String> operators = new ArrayList<String>();
556         String operatorList = removeQuotes(tokens.consume());
557         // Now split this string on ',' ...
558         for (String operatorValue : operatorList.split(",")) {
559             String operator = operatorValue.trim();
560             if (!VALID_QUERY_OPERATORS.contains(operator)) {
561                 throw new ParsingException(tokens.previousPosition(), CndI18n.expectedValidQueryOperator.text(operator));
562             }
563             operators.add(operator);
564         }
565         if (operators.isEmpty()) {
566             operators.addAll(VALID_QUERY_OPERATORS);
567         }
568         properties.add(propertyFactory.create(JcrLexicon.QUERY_OPERATORS, operators));
569     }
570 
571     /**
572      * Parse a node type's child node definition from the next tokens on the stream.
573      * 
574      * @param tokens the tokens containing the definition; never null
575      * @param nodeTypePath the path in the destination where the node type has been created, and under which the child node type
576      *        definitions should be placed
577      * @throws ParsingException if there is a problem parsing the content
578      */
579     protected void parseChildNodeDefinition( TokenStream tokens,
580                                              Path nodeTypePath ) {
581         tokens.consume('+');
582         Name name = parseName(tokens);
583         Path path = pathFactory.create(nodeTypePath, JcrLexicon.CHILD_NODE_DEFINITION);
584         List<Property> properties = new ArrayList<Property>();
585         properties.add(propertyFactory.create(JcrLexicon.NAME, name));
586 
587         parseRequiredPrimaryTypes(tokens, properties);
588         parseDefaultType(tokens, properties);
589         parseNodeAttributes(tokens, properties, name, path);
590 
591         // Create the node in the destination ...
592         destination.create(path, properties);
593     }
594 
595     /**
596      * Parse the child node definition's list of required primary types, if they appear next on the token stream.
597      * 
598      * @param tokens the tokens containing the definition; never null
599      * @param properties the list into which the property that represents the required types should be placed
600      * @throws ParsingException if there is a problem parsing the content
601      */
602     protected void parseRequiredPrimaryTypes( TokenStream tokens,
603                                               List<Property> properties ) {
604         if (tokens.canConsume('(')) {
605             List<Name> requiredTypes = parseNameList(tokens);
606             if (requiredTypes.isEmpty()) {
607                 requiredTypes.add(JcrNtLexicon.BASE);
608             }
609             properties.add(propertyFactory.create(JcrLexicon.REQUIRED_PRIMARY_TYPES, requiredTypes));
610             tokens.consume(')');
611         }
612     }
613 
614     /**
615      * Parse the child node definition's default type, if they appear next on the token stream.
616      * 
617      * @param tokens the tokens containing the definition; never null
618      * @param properties the list into which the property that represents the default primary type should be placed
619      * @throws ParsingException if there is a problem parsing the content
620      */
621     protected void parseDefaultType( TokenStream tokens,
622                                      List<Property> properties ) {
623         if (tokens.canConsume('=')) {
624             if (!tokens.canConsume('?')) {
625                 Name defaultType = parseName(tokens);
626                 properties.add(propertyFactory.create(JcrLexicon.DEFAULT_PRIMARY_TYPE, defaultType));
627             }
628         }
629     }
630 
631     /**
632      * Parse the child node definition's attributes, if they appear next on the token stream.
633      * 
634      * @param tokens the tokens containing the attributes; never null
635      * @param properties the list into which the properties that represents the attributes should be placed
636      * @param childNodeDefnName the name of the child node definition; never null
637      * @param childNodeDefnPath the path in the destination to the child node definition node; never null
638      * @throws ParsingException if there is a problem parsing the content
639      */
640     protected void parseNodeAttributes( TokenStream tokens,
641                                         List<Property> properties,
642                                         Name childNodeDefnName,
643                                         Path childNodeDefnPath ) {
644         boolean autoCreated = false;
645         boolean mandatory = false;
646         boolean isProtected = false;
647         boolean sns = false;
648         String onParentVersion = "COPY";
649         while (true) {
650             if (tokens.canConsumeAnyOf("AUTOCREATED", "AUT", "A")) {
651                 tokens.canConsume('?');
652                 autoCreated = true;
653             } else if (tokens.canConsumeAnyOf("MANDATORY", "MAN", "M")) {
654                 tokens.canConsume('?');
655                 mandatory = true;
656             } else if (tokens.canConsumeAnyOf("PROTECTED", "PRO", "P")) {
657                 tokens.canConsume('?');
658                 isProtected = true;
659             } else if (tokens.canConsumeAnyOf("SNS", "*")) { // standard JCR 2.0 keywords for SNS ...
660                 tokens.canConsume('?');
661                 sns = true;
662             } else if (jcr170 && tokens.canConsumeAnyOf("MULTIPLE", "MUL", "*")) { // from pre-JCR 2.0 ref impl
663                 tokens.canConsume('?');
664                 sns = true;
665             } else if (tokens.matchesAnyOf(VALID_ON_PARENT_VERSION)) {
666                 onParentVersion = tokens.consume();
667                 tokens.canConsume('?');
668             } else if (tokens.matches("OPV")) {
669                 // variant on-parent-version
670                 onParentVersion = tokens.consume();
671                 tokens.canConsume('?');
672             } else if (jcr170 && tokens.canConsumeAnyOf("PRIMARY", "PRI", "!")) {
673                 // Then this child node is considered the primary item ...
674                 Property primaryItem = propertyFactory.create(JcrLexicon.PRIMARY_ITEM_NAME, childNodeDefnName);
675                 destination.setProperties(childNodeDefnPath.getParent(), primaryItem);
676             } else {
677                 break;
678             }
679         }
680         properties.add(propertyFactory.create(JcrLexicon.AUTO_CREATED, autoCreated));
681         properties.add(propertyFactory.create(JcrLexicon.MANDATORY, mandatory));
682         properties.add(propertyFactory.create(JcrLexicon.PROTECTED, isProtected));
683         properties.add(propertyFactory.create(JcrLexicon.ON_PARENT_VERSION, onParentVersion.toUpperCase()));
684         properties.add(propertyFactory.create(JcrLexicon.SAME_NAME_SIBLINGS, sns));
685     }
686 
687     /**
688      * Parse the name that is expected to be next on the token stream.
689      * 
690      * @param tokens the tokens containing the name; never null
691      * @return the name; never null
692      * @throws ParsingException if there is a problem parsing the content
693      */
694     protected Name parseName( TokenStream tokens ) {
695         String value = tokens.consume();
696         try {
697             return nameFactory.create(removeQuotes(value));
698         } catch (ValueFormatException e) {
699             throw new ParsingException(tokens.previousPosition(), CndI18n.expectedValidNameLiteral.text(value));
700         }
701     }
702 
703     protected final String removeQuotes( String text ) {
704         // Remove leading and trailing quotes, if there are any ...
705         return text.replaceFirst("^['\"]+", "").replaceAll("['\"]+$", "");
706     }
707 }