View Javadoc
1   /*
2    * #%L
3    * DocumentElement.java - mongodb-async-driver - Allanbank Consulting, Inc.
4    * %%
5    * Copyright (C) 2011 - 2014 Allanbank Consulting, Inc.
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   * 
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package com.allanbank.mongodb.bson.element;
21  
22  import static com.allanbank.mongodb.util.Assertions.assertNotNull;
23  
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.regex.Pattern;
33  import java.util.regex.PatternSyntaxException;
34  
35  import com.allanbank.mongodb.bson.Document;
36  import com.allanbank.mongodb.bson.DocumentReference;
37  import com.allanbank.mongodb.bson.Element;
38  import com.allanbank.mongodb.bson.ElementType;
39  import com.allanbank.mongodb.bson.Visitor;
40  import com.allanbank.mongodb.bson.builder.BuilderFactory;
41  import com.allanbank.mongodb.bson.impl.EmptyDocument;
42  import com.allanbank.mongodb.bson.impl.RootDocument;
43  import com.allanbank.mongodb.bson.io.StringEncoder;
44  import com.allanbank.mongodb.util.PatternUtils;
45  
46  /**
47   * Wraps a single BSON document that may contain nested documents.
48   * 
49   * @api.yes This class is part of the driver's API. Public and protected members
50   *          will be deprecated for at least 1 non-bugfix release (version
51   *          numbers are <major>.<minor>.<bugfix>) before being
52   *          removed or modified.
53   * @copyright 2011-2013, Allanbank Consulting, Inc., All Rights Reserved
54   */
55  public class DocumentElement extends AbstractElement implements Document {
56  
57      /** The empty list of elements. */
58      public static final List<Element> EMPTY_ELEMENTS = Collections.emptyList();
59  
60      /** The BSON type for a document. */
61      public static final ElementType TYPE = ElementType.DOCUMENT;
62  
63      /** Serialization version for the class. */
64      private static final long serialVersionUID = -564259598403040796L;
65  
66      /**
67       * Computes and returns the number of bytes that are used to encode the
68       * element.
69       * 
70       * @param name
71       *            The name for the BSON array.
72       * @param entries
73       *            The entries in the array.
74       * @return The size of the element when encoded in bytes.
75       */
76      private static long computeSize(final String name,
77              final Collection<Element> entries) {
78          long result = 7; // type (1) + name null byte (1) + int length (4) +
79          // element null byte (1).
80          result += StringEncoder.utf8Size(name);
81          if ((entries != null) && !entries.isEmpty()) {
82              for (final Element element : entries) {
83                  result += element.size();
84              }
85          }
86  
87          return result;
88      }
89  
90      /**
91       * Computes and returns the number of bytes that are used to encode the
92       * element.
93       * 
94       * @param name
95       *            The name for the BSON array.
96       * @param documentSize
97       *            The size of the document used to construct the element.
98       * @return The size of the element when encoded in bytes.
99       */
100     private static long computeSize(final String name, final long documentSize) {
101         long result = 2; // type (1) + name null byte (1)
102         result += StringEncoder.utf8Size(name);
103         result += documentSize;
104 
105         return result;
106     }
107 
108     /**
109      * Constructed when a user tries to access the elements of the document by
110      * name.
111      */
112     private Map<String, Element> myElementMap;
113 
114     /** The elements of the document. */
115     private final List<Element> myElements;
116 
117     /**
118      * Constructs a new {@link DocumentElement}.
119      * 
120      * @param name
121      *            The name for the BSON document.
122      * @param elements
123      *            The sub-elements for the document.
124      * @throws IllegalArgumentException
125      *             If the {@code name} is <code>null</code>.
126      */
127     public DocumentElement(final String name, final Collection<Element> elements) {
128 
129         this(name, (elements != null) ? new ArrayList<Element>(elements)
130                 : EMPTY_ELEMENTS, true);
131     }
132 
133     /**
134      * Constructs a new {@link DocumentElement}.
135      * 
136      * @param name
137      *            The name for the BSON document.
138      * @param value
139      *            The document to copy elements from.
140      * @throws IllegalArgumentException
141      *             If the {@code name} or {@code value} is <code>null</code>.
142      */
143     public DocumentElement(final String name, final Document value) {
144         this(name, (value == null) ? EMPTY_ELEMENTS : value.getElements(),
145                 true, computeSize(name, (value == null) ? EmptyDocument.SIZE
146                         : value.size()));
147 
148         assertNotNull(value, "Document element's sub-document cannot be null.");
149     }
150 
151     /**
152      * Constructs a new {@link DocumentElement} with a single sub-document
153      * element.
154      * 
155      * @param name
156      *            The name for the BSON document.
157      * @param value
158      *            The document to copy elements from.
159      * @throws IllegalArgumentException
160      *             If the {@code name} or {@code value} is <code>null</code>.
161      */
162     public DocumentElement(final String name, final DocumentElement value) {
163         this(name, (value != null) ? Collections.singletonList((Element) value)
164                 : EMPTY_ELEMENTS, true);
165 
166         assertNotNull(value, "Document element's sub-document cannot be null.");
167     }
168 
169     /**
170      * Constructs a new {@link DocumentElement}.
171      * 
172      * @param name
173      *            The name for the BSON document.
174      * @param elements
175      *            The sub-elements for the document.
176      * @throws IllegalArgumentException
177      *             If the {@code name} is <code>null</code>.
178      */
179     public DocumentElement(final String name, final Element... elements) {
180         this(name, Arrays.asList(elements));
181     }
182 
183     /**
184      * Constructs a new {@link DocumentElement}.
185      * 
186      * @param name
187      *            The name for the BSON document.
188      * @param elements
189      *            The sub-elements for the document.
190      * @throws IllegalArgumentException
191      *             If the {@code name} is <code>null</code>.
192      */
193     public DocumentElement(final String name, final List<Element> elements) {
194         this(name, elements, false);
195     }
196 
197     /**
198      * Constructs a new {@link DocumentElement}.
199      * 
200      * @param name
201      *            The name for the BSON document.
202      * @param elements
203      *            The sub-elements for the document.
204      * @param takeOwnership
205      *            If true this element takes ownership of the list to avoid a
206      *            copy of the list.
207      */
208     public DocumentElement(final String name, final List<Element> elements,
209             final boolean takeOwnership) {
210         this(name, elements, takeOwnership, computeSize(name, elements));
211     }
212 
213     /**
214      * Constructs a new {@link DocumentElement}.
215      * 
216      * @param name
217      *            The name for the BSON document.
218      * @param elements
219      *            The sub-elements for the document.
220      * @param takeOwnership
221      *            If true this element takes ownership of the list to avoid a
222      *            copy of the list.
223      * @param size
224      *            The size of the element when encoded in bytes. If not known
225      *            then use the
226      *            {@link DocumentElement#DocumentElement(String, List, boolean)}
227      *            constructor instead.
228      */
229     public DocumentElement(final String name, final List<Element> elements,
230             final boolean takeOwnership, final long size) {
231 
232         super(name, size);
233 
234         if ((elements != null) && !elements.isEmpty()) {
235             if (takeOwnership) {
236                 myElements = Collections.unmodifiableList(elements);
237             }
238             else {
239                 myElements = Collections
240                         .unmodifiableList(new ArrayList<Element>(elements));
241             }
242         }
243         else {
244             myElements = EMPTY_ELEMENTS;
245         }
246     }
247 
248     /**
249      * Accepts the visitor and calls the {@link Visitor#visitDocument} method.
250      * 
251      * @see Element#accept(Visitor)
252      */
253     @Override
254     public void accept(final Visitor visitor) {
255         if (visitor instanceof SizeAwareVisitor) {
256             ((SizeAwareVisitor) visitor).visitDocument(getName(),
257                     getElements(), size());
258         }
259         else {
260             visitor.visitDocument(getName(), getElements());
261         }
262     }
263 
264     /**
265      * {@inheritDoc}
266      * <p>
267      * Returns this element.
268      * </p>
269      */
270     @Override
271     public Document asDocument() {
272         return this;
273     }
274 
275     /**
276      * Returns this sub-document as a {@link DocumentReference} if it conforms
277      * to the MongoDB DBRef convention. Returns <code>null</code> otherwise.
278      * <p>
279      * A DocumentReference contains (order matters):
280      * <ol>
281      * <li>The name of the collection where the referenced document resides:
282      * {@code $ref}.</li>
283      * <li>The value of the _id field in the referenced document: {@code $id}.</li>
284      * <li>The name of the database where the referenced document resides:
285      * {@code $db} (Optional).</li>
286      * </ol>
287      * 
288      * @return This sub-document as a {@link DocumentReference} if it conforms
289      *         to the MongoDB DBRef convention. Returns <code>null</code>
290      *         otherwise.
291      * 
292      * @see #isDocumentReference()
293      * @see <a
294      *      href="http://docs.mongodb.org/manual/applications/database-references/#dbref">MongoDB
295      *      DBRef Information</a>
296      */
297     public DocumentReference asDocumentReference() {
298         final int elementCount = myElements.size();
299         if (elementCount == 2) {
300             final Element element1 = myElements.get(0);
301             final Element element2 = myElements.get(1);
302 
303             final String element1Name = element1.getName();
304             final ElementType element1Type = element1.getType();
305             final String element2Name = element2.getName();
306 
307             if (DocumentReference.COLLECTION_FIELD_NAME.equals(element1Name)
308                     && DocumentReference.ID_FIELD_NAME.equals(element2Name)) {
309                 if (element1Type == ElementType.STRING) {
310                     return new DocumentReference(
311                             ((StringElement) element1).getValue(), element2);
312                 }
313                 else if (element1Type == ElementType.SYMBOL) {
314                     return new DocumentReference(
315                             ((SymbolElement) element1).getSymbol(), element2);
316                 }
317             }
318         }
319         else if (myElements.size() == 3) {
320             final Element element1 = myElements.get(0);
321             final Element element2 = myElements.get(1);
322             final Element element3 = myElements.get(2);
323 
324             final String element1Name = element1.getName();
325             final ElementType element1Type = element1.getType();
326             final String element2Name = element2.getName();
327             final String element3Name = element3.getName();
328             final ElementType element3Type = element3.getType();
329 
330             if (DocumentReference.COLLECTION_FIELD_NAME.equals(element1Name)
331                     && DocumentReference.ID_FIELD_NAME.equals(element2Name)
332                     && DocumentReference.DATABASE_FIELD_NAME
333                             .equals(element3Name)) {
334                 if (element1Type == ElementType.STRING) {
335                     if (element3Type == ElementType.STRING) {
336                         return new DocumentReference(
337                                 ((StringElement) element3).getValue(),
338                                 ((StringElement) element1).getValue(), element2);
339                     }
340                     else if (element3Type == ElementType.SYMBOL) {
341                         return new DocumentReference(
342                                 ((SymbolElement) element3).getSymbol(),
343                                 ((StringElement) element1).getValue(), element2);
344                     }
345                 }
346                 else if (element1Type == ElementType.SYMBOL) {
347                     if (element3Type == ElementType.STRING) {
348                         return new DocumentReference(
349                                 ((StringElement) element3).getValue(),
350                                 ((SymbolElement) element1).getSymbol(),
351                                 element2);
352                     }
353                     else if (element3Type == ElementType.SYMBOL) {
354                         return new DocumentReference(
355                                 ((SymbolElement) element3).getSymbol(),
356                                 ((SymbolElement) element1).getSymbol(),
357                                 element2);
358                     }
359                 }
360             }
361         }
362         return null;
363     }
364 
365     /**
366      * {@inheritDoc}
367      * <p>
368      * Overridden to compare the elements of the document if the base class
369      * comparison is equals.
370      * </p>
371      */
372     @Override
373     public int compareTo(final Element otherElement) {
374         int result = super.compareTo(otherElement);
375 
376         if (result == 0) {
377             final DocumentElement other = (DocumentElement) otherElement;
378             final int length = Math.min(myElements.size(),
379                     other.myElements.size());
380             for (int i = 0; i < length; ++i) {
381                 result = myElements.get(i).compareTo(other.myElements.get(i));
382                 if (result != 0) {
383                     return result;
384                 }
385             }
386 
387             result = myElements.size() - other.myElements.size();
388         }
389 
390         return result;
391     }
392 
393     /**
394      * Returns true if the document contains an element with the specified name.
395      * 
396      * @param name
397      *            The name of the element to locate.
398      * @return True if the document contains an element with the given name,
399      *         false otherwise.
400      * @see Document#contains(String)
401      */
402     @Override
403     public boolean contains(final String name) {
404         return getElementMap().containsKey(name);
405     }
406 
407     /**
408      * Determines if the passed object is of this same type as this object and
409      * if so that its fields are equal.
410      * 
411      * @param object
412      *            The object to compare to.
413      * 
414      * @see java.lang.Object#equals(java.lang.Object)
415      */
416     @Override
417     public boolean equals(final Object object) {
418         boolean result = false;
419         if (this == object) {
420             result = true;
421         }
422         else if ((object != null) && (getClass() == object.getClass())) {
423             final DocumentElement other = (DocumentElement) object;
424 
425             result = super.equals(object)
426                     && myElements.equals(other.myElements);
427         }
428         return result;
429     }
430 
431     /**
432      * {@inheritDoc}
433      * <p>
434      * Searches this sub-elements for matching elements on the path and are of
435      * the right type.
436      * </p>
437      * 
438      * @see Element#find
439      */
440     @Override
441     public <E extends Element> List<E> find(final Class<E> clazz,
442             final String... nameRegexs) {
443         List<E> elements = Collections.emptyList();
444 
445         if (0 < nameRegexs.length) {
446             final String nameRegex = nameRegexs[0];
447             final String[] subNameRegexs = Arrays.copyOfRange(nameRegexs, 1,
448                     nameRegexs.length);
449 
450             elements = new ArrayList<E>();
451             try {
452                 final Pattern pattern = PatternUtils.toPattern(nameRegex);
453                 for (final Element element : myElements) {
454                     if (pattern.matcher(element.getName()).matches()) {
455                         elements.addAll(element.find(clazz, subNameRegexs));
456                     }
457                 }
458             }
459             catch (final PatternSyntaxException pse) {
460                 // Assume a non-pattern?
461                 for (final Element element : myElements) {
462                     if (nameRegex.equals(element.getName())) {
463                         elements.addAll(element.find(clazz, subNameRegexs));
464                     }
465                 }
466             }
467         }
468         else {
469             // End of the path -- are we the right type/element?
470             if (clazz.isAssignableFrom(this.getClass())) {
471                 elements = Collections.singletonList(clazz.cast(this));
472             }
473         }
474         return elements;
475     }
476 
477     /**
478      * {@inheritDoc}
479      * <p>
480      * Searches this sub-elements for matching elements on the path and are of
481      * the right type.
482      * </p>
483      * 
484      * @see Element#findFirst
485      */
486     @Override
487     public <E extends Element> E findFirst(final Class<E> clazz,
488             final String... nameRegexs) {
489         E element = null;
490         if (0 < nameRegexs.length) {
491             final String nameRegex = nameRegexs[0];
492             final String[] subNameRegexs = Arrays.copyOfRange(nameRegexs, 1,
493                     nameRegexs.length);
494 
495             try {
496                 final Pattern pattern = PatternUtils.toPattern(nameRegex);
497                 final Iterator<Element> iter = myElements.iterator();
498                 while (iter.hasNext() && (element == null)) {
499                     final Element docElement = iter.next();
500                     if (pattern.matcher(docElement.getName()).matches()) {
501                         element = docElement.findFirst(clazz, subNameRegexs);
502                     }
503                 }
504             }
505             catch (final PatternSyntaxException pse) {
506                 // Assume a non-pattern?
507                 final Iterator<Element> iter = myElements.iterator();
508                 while (iter.hasNext() && (element == null)) {
509                     final Element docElement = iter.next();
510                     if (nameRegex.equals(docElement.getName())) {
511                         element = docElement.findFirst(clazz, subNameRegexs);
512                     }
513                 }
514             }
515         }
516         else {
517             // End of the path -- are we the right type/element?
518             if (clazz.isAssignableFrom(this.getClass())) {
519                 element = clazz.cast(this);
520             }
521         }
522         return element;
523     }
524 
525     /**
526      * Returns the element with the specified name and type or null if no
527      * element with that name and type exists.
528      * 
529      * @param <E>
530      *            The type of element to get.
531      * @param clazz
532      *            The class of element to get.
533      * @param name
534      *            The name of the element to locate.
535      * @return The sub-element in the document with the given name or null if
536      *         element exists with the given name.
537      * @see Document#get(Class, String)
538      */
539     @Override
540     public <E extends Element> E get(final Class<E> clazz, final String name) {
541         final Element element = get(name);
542         if ((element != null) && clazz.isAssignableFrom(element.getClass())) {
543             return clazz.cast(element);
544         }
545         return null;
546     }
547 
548     /**
549      * Returns the element with the specified name or null if no element with
550      * that name exists.
551      * 
552      * @param name
553      *            The name of the element to locate.
554      * @return The sub-element in the document with the given name or null if
555      *         element exists with the given name.
556      * @see Document#get(String)
557      */
558     @Override
559     public Element get(final String name) {
560         return getElementMap().get(name);
561     }
562 
563     /**
564      * Returns the element's document.
565      * 
566      * @return The document contained within the element.
567      */
568     public Document getDocument() {
569         return new RootDocument(myElements);
570     }
571 
572     /**
573      * Returns the elements in the document.
574      * 
575      * @return The elements in the document.
576      */
577     @Override
578     public List<Element> getElements() {
579         return myElements;
580     }
581 
582     /**
583      * {@inheritDoc}
584      */
585     @Override
586     public ElementType getType() {
587         return TYPE;
588     }
589 
590     /**
591      * {@inheritDoc}
592      * <p>
593      * Returns a stand-alone {@link Document}.
594      * </p>
595      */
596     @Override
597     public Document getValueAsObject() {
598         return BuilderFactory.start(this).build();
599     }
600 
601     /**
602      * Computes a reasonable hash code.
603      * 
604      * @return The hash code value.
605      */
606     @Override
607     public int hashCode() {
608         int result = 1;
609         result = (31 * result) + super.hashCode();
610         result = (31 * result) + myElements.hashCode();
611         return result;
612     }
613 
614     /**
615      * Returns true if this sub-document conforms to the MongoDB DBRef
616      * convention, false otherwise.
617      * <p>
618      * A DocumentReference contains (order matters):
619      * <ol>
620      * <li>The name (string or symbol) of the collection where the referenced
621      * document resides: {@code $ref}.</li>
622      * <li>The value of the _id field in the referenced document: {@code $id}.</li>
623      * <li>The name (string or symbol) of the database where the referenced
624      * document resides: {@code $db} (Optional).</li>
625      * </ol>
626      * 
627      * @return True if this sub-document conforms to the MongoDB DBRef
628      *         convention, false otherwise.
629      * 
630      * @see #asDocumentReference()
631      * @see DocumentReference
632      * @see <a
633      *      href="http://docs.mongodb.org/manual/applications/database-references/#dbref">MongoDB
634      *      DBRef Information</a>
635      */
636     public boolean isDocumentReference() {
637         final int elementCount = myElements.size();
638         if (elementCount == 2) {
639             final Element element1 = myElements.get(0);
640             final Element element2 = myElements.get(1);
641 
642             final String element1Name = element1.getName();
643             final ElementType element1Type = element1.getType();
644             final String element2Name = element2.getName();
645 
646             return DocumentReference.COLLECTION_FIELD_NAME.equals(element1Name)
647                     && ((element1Type == ElementType.STRING) || (element1Type == ElementType.SYMBOL))
648                     && DocumentReference.ID_FIELD_NAME.equals(element2Name);
649         }
650         else if (myElements.size() == 3) {
651             final Element element1 = myElements.get(0);
652             final Element element2 = myElements.get(1);
653             final Element element3 = myElements.get(2);
654 
655             final String element1Name = element1.getName();
656             final ElementType element1Type = element1.getType();
657             final String element2Name = element2.getName();
658             final String element3Name = element3.getName();
659             final ElementType element3Type = element3.getType();
660 
661             return DocumentReference.COLLECTION_FIELD_NAME.equals(element1Name)
662                     && ((element1Type == ElementType.STRING) || (element1Type == ElementType.SYMBOL))
663                     && DocumentReference.ID_FIELD_NAME.equals(element2Name)
664                     && DocumentReference.DATABASE_FIELD_NAME
665                             .equals(element3Name)
666                     && ((element3Type == ElementType.STRING) || (element3Type == ElementType.SYMBOL));
667 
668         }
669         return false;
670     }
671 
672     /**
673      * Returns an iterator over the documents elements.
674      * 
675      * @see Iterable#iterator()
676      */
677     @Override
678     public Iterator<Element> iterator() {
679         return getElements().iterator();
680     }
681 
682     /**
683      * {@inheritDoc}
684      * <p>
685      * Returns a new {@link DocumentElement}.
686      * </p>
687      */
688     @Override
689     public DocumentElement withName(final String name) {
690         if (getName().equals(name)) {
691             return this;
692         }
693         return new DocumentElement(name, myElements);
694     }
695 
696     /**
697      * Returns a map from the element names to the elements in the document.
698      * Used for faster by-name access.
699      * 
700      * @return The element name to element mapping.
701      */
702     private Map<String, Element> getElementMap() {
703         if (myElementMap == null) {
704             final List<Element> elements = myElements;
705             final Map<String, Element> mapping = new HashMap<String, Element>(
706                     elements.size() + elements.size());
707 
708             for (final Element element : elements) {
709                 mapping.put(element.getName(), element);
710             }
711 
712             // Swap the finished map into position.
713             myElementMap = mapping;
714         }
715 
716         return myElementMap;
717     }
718 }