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 }