View Javadoc
1   /*
2    * #%L
3    * ArrayElement.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 java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.Iterator;
26  import java.util.List;
27  import java.util.regex.Pattern;
28  import java.util.regex.PatternSyntaxException;
29  
30  import com.allanbank.mongodb.bson.Element;
31  import com.allanbank.mongodb.bson.ElementType;
32  import com.allanbank.mongodb.bson.Visitor;
33  import com.allanbank.mongodb.bson.io.StringEncoder;
34  import com.allanbank.mongodb.util.PatternUtils;
35  
36  /**
37   * A wrapper for a BSON array.
38   * 
39   * @api.yes This class is part of the driver's API. Public and protected members
40   *          will be deprecated for at least 1 non-bugfix release (version
41   *          numbers are <major>.<minor>.<bugfix>) before being
42   *          removed or modified.
43   * @copyright 2011-2013, Allanbank Consulting, Inc., All Rights Reserved
44   */
45  public class ArrayElement extends AbstractElement {
46  
47      /** The BSON type for an array. */
48      public static final ElementType TYPE = ElementType.ARRAY;
49  
50      /** A cache of the names for the elements at an index to be used. */
51      private static final String[] ourIndexes;
52  
53      /** Serialization version for the class. */
54      private static final long serialVersionUID = -7363294574214059703L;
55      static {
56          ourIndexes = new String[1000];
57  
58          for (int i = 0; i < ourIndexes.length; ++i) {
59              ourIndexes[i] = Integer.toString(i).intern();
60          }
61      }
62  
63      /**
64       * Similar to the caching of Integer object values for a range we cache the
65       * index names for an array's first 256 positions.
66       * 
67       * @param index
68       *            The index for the array element.
69       * @return The name of the value at that index.
70       */
71      public static final String nameFor(final int index) {
72          if ((0 <= index) && (index < ourIndexes.length)) {
73              return ourIndexes[index];
74          }
75          return Integer.toString(index);
76      }
77  
78      /**
79       * Computes and returns the number of bytes that are used to encode the
80       * element.
81       * 
82       * @param name
83       *            The name for the BSON array.
84       * @param entries
85       *            The entries in the array.
86       * @return The size of the element when encoded in bytes.
87       */
88      private static long computeSize(final String name,
89              final List<Element> entries) {
90          long result = 7; // type (1) + name null byte (1) + int length (4) +
91          // elements null byte (1).
92          result += StringEncoder.utf8Size(name);
93          if ((entries != null) && !entries.isEmpty()) {
94              for (final Element element : entries) {
95                  result += element.size();
96              }
97          }
98  
99          return result;
100     }
101 
102     /**
103      * The entries in the array. The name attribute will be ignored when
104      * encoding the elements.
105      */
106     private final List<Element> myEntries;
107 
108     /**
109      * Constructs a new {@link ArrayElement}.
110      * 
111      * @param name
112      *            The name for the BSON array.
113      * @param entries
114      *            The entries in the array.
115      * @throws IllegalArgumentException
116      *             If the {@code name} is <code>null</code>.
117      */
118     public ArrayElement(final String name, final Element... entries)
119             throws IllegalArgumentException {
120         this(name, Arrays.asList(entries));
121     }
122 
123     /**
124      * Constructs a new {@link ArrayElement}.
125      * 
126      * @param name
127      *            The name for the BSON array.
128      * @param entries
129      *            The entries in the array.
130      * @throws IllegalArgumentException
131      *             If the {@code name} is <code>null</code>.
132      */
133     public ArrayElement(final String name, final List<Element> entries)
134             throws IllegalArgumentException {
135         this(name, entries, computeSize(name, entries));
136     }
137 
138     /**
139      * Constructs a new {@link ArrayElement}.
140      * 
141      * @param name
142      *            The name for the BSON array.
143      * @param entries
144      *            The entries in the array.
145      * @param size
146      *            The size of the element when encoded in bytes. If not known
147      *            then use the {@link ArrayElement#ArrayElement(String, List)}
148      *            constructor instead.
149      * @throws IllegalArgumentException
150      *             If the {@code name} is <code>null</code>.
151      */
152     public ArrayElement(final String name, final List<Element> entries,
153             final long size) throws IllegalArgumentException {
154         super(name, size);
155 
156         if ((entries != null) && !entries.isEmpty()) {
157             // The names of the elements have to be a specific value.
158             final int length = entries.size();
159             final List<Element> elements = new ArrayList<Element>(length);
160             int index = 0;
161             for (final Element element : entries) {
162                 final Element withCorrectName = element
163                         .withName(nameFor(index));
164                 elements.add(withCorrectName);
165                 index += 1;
166             }
167 
168             myEntries = Collections.unmodifiableList(elements);
169         }
170         else {
171             myEntries = Collections.emptyList();
172         }
173     }
174 
175     /**
176      * Accepts the visitor and calls the
177      * {@link Visitor#visitArray(String, List)} method.
178      * 
179      * @see Element#accept(Visitor)
180      */
181     @Override
182     public void accept(final Visitor visitor) {
183         if (visitor instanceof SizeAwareVisitor) {
184             ((SizeAwareVisitor) visitor).visitArray(getName(), getEntries(),
185                     size());
186         }
187         else {
188             visitor.visitArray(getName(), getEntries());
189         }
190     }
191 
192     /**
193      * {@inheritDoc}
194      * <p>
195      * Overridden to compare the elements of the array if the base class
196      * comparison is equals.
197      * </p>
198      */
199     @Override
200     public int compareTo(final Element otherElement) {
201         int result = super.compareTo(otherElement);
202 
203         if (result == 0) {
204             final ArrayElement other = (ArrayElement) otherElement;
205             final int length = Math.min(myEntries.size(),
206                     other.myEntries.size());
207             for (int i = 0; i < length; ++i) {
208                 result = myEntries.get(i).compareTo(other.myEntries.get(i));
209                 if (result != 0) {
210                     return result;
211                 }
212             }
213 
214             result = myEntries.size() - other.myEntries.size();
215         }
216 
217         return result;
218     }
219 
220     /**
221      * Determines if the passed object is of this same type as this object and
222      * if so that its fields are equal.
223      * 
224      * @param object
225      *            The object to compare to.
226      * 
227      * @see java.lang.Object#equals(java.lang.Object)
228      */
229     @Override
230     public boolean equals(final Object object) {
231         boolean result = false;
232         if (this == object) {
233             result = true;
234         }
235         else if ((object != null) && (getClass() == object.getClass())) {
236             final ArrayElement other = (ArrayElement) object;
237 
238             result = super.equals(object) && myEntries.equals(other.myEntries);
239         }
240         return result;
241     }
242 
243     /**
244      * {@inheritDoc}
245      * <p>
246      * Searches this sub-elements for matching elements on the path and are of
247      * the right type.
248      * </p>
249      * 
250      * @see Element#find
251      */
252     @Override
253     public <E extends Element> List<E> find(final Class<E> clazz,
254             final String... nameRegexs) {
255         if (0 < nameRegexs.length) {
256             final List<E> elements = new ArrayList<E>();
257             final String nameRegex = nameRegexs[0];
258             final String[] subNameRegexs = Arrays.copyOfRange(nameRegexs, 1,
259                     nameRegexs.length);
260             try {
261                 final Pattern pattern = PatternUtils.toPattern(nameRegex);
262                 for (final Element element : myEntries) {
263                     if (pattern.matcher(element.getName()).matches()) {
264                         elements.addAll(element.find(clazz, subNameRegexs));
265                     }
266                 }
267             }
268             catch (final PatternSyntaxException pse) {
269                 // Assume a non-pattern?
270                 for (final Element element : myEntries) {
271                     if (nameRegex.equals(element.getName())) {
272                         elements.addAll(element.find(clazz, subNameRegexs));
273                     }
274                 }
275             }
276 
277             return elements;
278         }
279 
280         // End of the path -- are we the right type
281         if (clazz.isAssignableFrom(this.getClass())) {
282             return Collections.singletonList(clazz.cast(this));
283         }
284         return Collections.emptyList();
285     }
286 
287     /**
288      * {@inheritDoc}
289      * <p>
290      * Searches this sub-elements for matching elements on the path and are of
291      * the right type.
292      * </p>
293      * 
294      * @see Element#findFirst
295      */
296     @Override
297     public <E extends Element> E findFirst(final Class<E> clazz,
298             final String... nameRegexs) {
299         E element = null;
300         if (0 < nameRegexs.length) {
301             final String nameRegex = nameRegexs[0];
302             final String[] subNameRegexs = Arrays.copyOfRange(nameRegexs, 1,
303                     nameRegexs.length);
304 
305             try {
306                 final Pattern pattern = PatternUtils.toPattern(nameRegex);
307                 final Iterator<Element> iter = myEntries.iterator();
308                 while (iter.hasNext() && (element == null)) {
309                     final Element docElement = iter.next();
310                     if (pattern.matcher(docElement.getName()).matches()) {
311                         element = docElement.findFirst(clazz, subNameRegexs);
312                     }
313                 }
314             }
315             catch (final PatternSyntaxException pse) {
316                 // Assume a non-pattern?
317                 final Iterator<Element> iter = myEntries.iterator();
318                 while (iter.hasNext() && (element == null)) {
319                     final Element docElement = iter.next();
320                     if (nameRegex.equals(docElement.getName())) {
321                         element = docElement.findFirst(clazz, subNameRegexs);
322                     }
323                 }
324             }
325         }
326         else {
327             // End of the path -- are we the right type/element?
328             if (clazz.isAssignableFrom(this.getClass())) {
329                 element = clazz.cast(this);
330             }
331         }
332         return element;
333     }
334 
335     /**
336      * Returns the entries in the array. The name attribute will be ignored when
337      * encoding the elements. When decoded the names will be the strings 0, 1,
338      * 2, 3, etc..
339      * 
340      * @return The entries in the array.
341      */
342     public List<Element> getEntries() {
343         return myEntries;
344     }
345 
346     /**
347      * {@inheritDoc}
348      */
349     @Override
350     public ElementType getType() {
351         return TYPE;
352     }
353 
354     /**
355      * {@inheritDoc}
356      * <p>
357      * Returns an Element[].
358      * </p>
359      */
360     @Override
361     public Element[] getValueAsObject() {
362         return myEntries.toArray(new Element[myEntries.size()]);
363     }
364 
365     /**
366      * Computes a reasonable hash code.
367      * 
368      * @return The hash code value.
369      */
370     @Override
371     public int hashCode() {
372         int result = 1;
373         result = (31 * result) + super.hashCode();
374         result = (31 * result) + myEntries.hashCode();
375         return result;
376     }
377 
378     /**
379      * {@inheritDoc}
380      * <p>
381      * Returns a new {@link ArrayElement}.
382      * </p>
383      */
384     @Override
385     public ArrayElement withName(final String name) {
386         if (getName().equals(name)) {
387             return this;
388         }
389         return new ArrayElement(name, myEntries);
390     }
391 
392 }