View Javadoc
1   /*
2    * Copyright (c) 2012-2022, jcabi.com
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met: 1) Redistributions of source code must retain the above
8    * copyright notice, this list of conditions and the following
9    * disclaimer. 2) Redistributions in binary form must reproduce the above
10   * copyright notice, this list of conditions and the following
11   * disclaimer in the documentation and/or other materials provided
12   * with the distribution. 3) Neither the name of the jcabi.com nor
13   * the names of its contributors may be used to endorse or promote
14   * products derived from this software without specific prior written
15   * permission.
16   *
17   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
19   * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21   * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
22   * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28   * OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package com.jcabi.xml;
31  
32  import com.jcabi.log.Logger;
33  import java.util.Collection;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.ListIterator;
37  import org.w3c.dom.Node;
38  
39  /**
40   * Wrapper of {@link List}.
41   *
42   * <p>This wrapper is our internal implementation of a {@link List}. The only
43   * purpose of this wrapper is to throw our own custom exception when the client
44   * is trying to access an element that is absent in the list. Such a custom
45   * exception ({@link ListWrapper.NodeNotFoundException})
46   * includes detailed information about
47   * the original document. Thus, such an incorrect list-access operation will
48   * lead to an exception that contains all the details inside (not just a simple
49   * error message). For example:
50   *
51   * <pre> String name = new XMLDocument("...")
52   *   .xpath("/document/name/text()")
53   *   .get(0)</pre>
54   *
55   * <p>This snippet is trying to get a text value from XML element, and there
56   * is no guarantee that such an element exists in the document. In order to give
57   * a detailed report to the user of the problem (including a full content of
58   * the XML document) we're returning {@link ListWrapper} from
59   * {@link XMLDocument#xpath(String)}. The only method that we implement
60   * for this purpose is {@link #get(int)}.
61   *
62   * <p>{@link ListWrapper} is an unmodifiable list, that's why
63   * the majority of inherited method are not implemented and
64   * thow runtime exceptions if being called.
65   *
66   * <p>The method {@link #get(int)} throws
67   * {@link ListWrapper.NodeNotFoundException}
68   * if such an element doesn't exist in the list.
69   *
70   * <p>The method {@link #subList(int, int)}
71   * throws {@link ListWrapper.NodeNotFoundException}
72   * when either
73   * {@code start} or {@code end} is bigger than the size of the list. In all
74   * other cases of illegal method call (start is less than zero, end is
75   * less than zero, or start is bigger than end) a standard
76   * {@link IndexOutOfBoundsException} is thrown (by the encapsulated
77   * implementation of {@Link List}).
78   *
79   * <p>The class is immutable and thread-safe.
80   *
81   * @since 0.1
82   * @param <T> Time of items
83   */
84  @SuppressWarnings("PMD.TooManyMethods")
85  final class ListWrapper<T> implements List<T> {
86      /**
87       * The original list.
88       */
89      private final transient List<T> original;
90  
91      /**
92       * The XML where this list came from.
93       */
94      private final transient Node dom;
95  
96      /**
97       * XPath.
98       */
99      private final transient String xpath;
100 
101     /**
102      * Public ctor.
103      * @param list Original list
104      * @param node The XML
105      * @param addr Address
106      */
107     ListWrapper(final List<T> list, final Node node, final String addr) {
108         super();
109         this.original = list;
110         this.dom = node;
111         this.xpath = addr;
112     }
113 
114     @Override
115     public String toString() {
116         return this.original.toString();
117     }
118 
119     @Override
120     public boolean add(final T element) {
121         throw new UnsupportedOperationException("#add(T)");
122     }
123 
124     @Override
125     public void add(final int index, final T element) {
126         throw new UnsupportedOperationException("#add(int, T)");
127     }
128 
129     @Override
130     public boolean addAll(final Collection<? extends T> elements) {
131         throw new UnsupportedOperationException("#addAll(Collection)");
132     }
133 
134     @Override
135     public boolean addAll(final int index, final Collection<? extends T> elms) {
136         throw new UnsupportedOperationException("#add(int, Collection)");
137     }
138 
139     @Override
140     public void clear() {
141         throw new UnsupportedOperationException("#clear()");
142     }
143 
144     @Override
145     public boolean contains(final Object element) {
146         return this.original.contains(element);
147     }
148 
149     @Override
150     public boolean containsAll(final Collection<?> elements) {
151         return this.original.containsAll(elements);
152     }
153 
154     @Override
155     public T get(final int index) {
156         if (index >= this.size()) {
157             throw new ListWrapper.NodeNotFoundException(
158                 String.format(
159                     "Index (%d) is out of bounds (size=%d)",
160                     index, this.size()
161                 ),
162                 this.dom,
163                 this.xpath
164             );
165         }
166         return this.original.get(index);
167     }
168 
169     @Override
170     public int indexOf(final Object element) {
171         return this.original.indexOf(element);
172     }
173 
174     @Override
175     public boolean isEmpty() {
176         return this.original.isEmpty();
177     }
178 
179     @Override
180     public Iterator<T> iterator() {
181         return this.original.iterator();
182     }
183 
184     @Override
185     public int lastIndexOf(final Object element) {
186         return this.original.lastIndexOf(element);
187     }
188 
189     @Override
190     public ListIterator<T> listIterator() {
191         return this.original.listIterator();
192     }
193 
194     @Override
195     public ListIterator<T> listIterator(final int index) {
196         return this.original.listIterator(index);
197     }
198 
199     @Override
200     public boolean equals(final Object other) {
201         return this.original.equals(other);
202     }
203 
204     @Override
205     public int hashCode() {
206         return this.original.hashCode();
207     }
208 
209     @Override
210     public T remove(final int index) {
211         throw new UnsupportedOperationException("#remove(int)");
212     }
213 
214     @Override
215     public boolean remove(final Object element) {
216         throw new UnsupportedOperationException("#remove(Object)");
217     }
218 
219     @Override
220     public boolean removeAll(final Collection<?> elements) {
221         throw new UnsupportedOperationException("#removeAll(Collection)");
222     }
223 
224     @Override
225     public boolean retainAll(final Collection<?> elements) {
226         throw new UnsupportedOperationException("#retainAll(Collection)");
227     }
228 
229     @Override
230     public T set(final int index, final T element) {
231         throw new UnsupportedOperationException("#set(int, T)");
232     }
233 
234     @Override
235     public int size() {
236         return this.original.size();
237     }
238 
239     @Override
240     public List<T> subList(final int start, final int end) {
241         if (start >= this.size()) {
242             throw new ListWrapper.NodeNotFoundException(
243                 String.format(
244                     "Start of subList (%d) is out of bounds (size=%d)",
245                     start, this.size()
246                 ),
247                 this.dom,
248                 this.xpath
249             );
250         }
251         if (end >= this.size()) {
252             throw new ListWrapper.NodeNotFoundException(
253                 String.format(
254                     "End of subList (%d) is out of bounds (size=%d)",
255                     end, this.size()
256                 ),
257                 this.dom,
258                 this.xpath
259             );
260         }
261         return this.original.subList(start, end);
262     }
263 
264     @Override
265     public Object[] toArray() {
266         return this.original.toArray();
267     }
268 
269     @Override
270     @SuppressWarnings("PMD.UseVarargs")
271     public <E> E[] toArray(final E[] array) {
272         return this.original.toArray(array);
273     }
274 
275     /**
276      * Node not found in XmlDocument.
277      *
278      * @since 0.1
279      */
280     private static final class NodeNotFoundException
281         extends IndexOutOfBoundsException {
282         /**
283          * Serialization marker.
284          */
285         private static final long serialVersionUID = 0x7526FA78EEDAC470L;
286 
287         /**
288          * Public ctor.
289          * @param message Error message
290          * @param node The XML with error
291          * @param query The query in XPath
292          */
293         NodeNotFoundException(final String message, final Node node,
294             final CharSequence query) {
295             super(
296                 Logger.format(
297                     "XPath '%s' not found in '%[text]s': %s",
298                     ListWrapper.NodeNotFoundException.escapeUnicode(query),
299                     ListWrapper.NodeNotFoundException.escapeUnicode(
300                         new XMLDocument(node).toString()
301                 ),
302                     message
303             )
304             );
305         }
306 
307         /**
308          * Escape unicode characters.
309          * @param input Input string
310          * @return Escaped output
311          */
312         private static String escapeUnicode(final CharSequence input) {
313             final int length = input.length();
314             final StringBuilder output = new StringBuilder(length);
315             for (int index = 0; index < length; index += 1) {
316                 final char character = input.charAt(index);
317                 if (character < 32 || character > 0x7f) {
318                     output.append(String.format("\\u%X", (int) character));
319                 } else {
320                     output.append(character);
321                 }
322             }
323             return output.toString();
324         }
325     }
326 
327 }