View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2012-2025 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.jcabi.xml;
6   
7   import com.jcabi.log.Logger;
8   import java.util.Collection;
9   import java.util.Iterator;
10  import java.util.List;
11  import java.util.ListIterator;
12  import org.w3c.dom.Node;
13  
14  /**
15   * Wrapper of {@link List}.
16   * @param <T> Time of items
17   *
18   * <p>This wrapper is our internal implementation of a {@link List}. The only
19   * purpose of this wrapper is to throw our own custom exception when the client
20   * is trying to access an element that is absent in the list. Such a custom
21   * exception ({@link ListWrapper.NodeNotFoundException})
22   * includes detailed information about
23   * the original document. Thus, such an incorrect list-access operation will
24   * lead to an exception that contains all the details inside (not just a simple
25   * error message). For example:
26   *
27   * <pre> String name = new XMLDocument("...")
28   *   .xpath("/document/name/text()")
29   *   .get(0)</pre>
30   *
31   * <p>This snippet is trying to get a text value from XML element, and there
32   * is no guarantee that such an element exists in the document. In order to give
33   * a detailed report to the user of the problem (including a full content of
34   * the XML document) we're returning {@link ListWrapper} from
35   * {@link XMLDocument#xpath(String)}. The only method that we implement
36   * for this purpose is {@link #get(int)}.
37   *
38   * <p>{@link ListWrapper} is an unmodifiable list, that's why
39   * the majority of inherited method are not implemented and
40   * thow runtime exceptions if being called.
41   *
42   * <p>The method {@link #get(int)} throws
43   * {@link ListWrapper.NodeNotFoundException}
44   * if such an element doesn't exist in the list.
45   *
46   * <p>The method {@link #subList(int, int)}
47   * throws {@link ListWrapper.NodeNotFoundException}
48   * when either
49   * {@code start} or {@code end} is bigger than the size of the list. In all
50   * other cases of illegal method call (start is less than zero, end is
51   * less than zero, or start is bigger than end) a standard
52   * {@link IndexOutOfBoundsException} is thrown (by the encapsulated
53   * implementation of {@Link List}).
54   *
55   * <p>The class is immutable and thread-safe.
56   *
57   * @since 0.1
58   */
59  @SuppressWarnings("PMD.TooManyMethods")
60  final class ListWrapper<T> implements List<T> {
61      /**
62       * The original list.
63       */
64      private final transient List<T> original;
65  
66      /**
67       * The XML where this list came from.
68       */
69      private final transient Node dom;
70  
71      /**
72       * XPath.
73       */
74      private final transient String xpath;
75  
76      /**
77       * Public ctor.
78       * @param list Original list
79       * @param node The XML
80       * @param addr Address
81       */
82      ListWrapper(final List<T> list, final Node node, final String addr) {
83          super();
84          this.original = list;
85          this.dom = node;
86          this.xpath = addr;
87      }
88  
89      @Override
90      public String toString() {
91          return this.original.toString();
92      }
93  
94      @Override
95      public boolean add(final T element) {
96          throw new UnsupportedOperationException("#add(T)");
97      }
98  
99      @Override
100     public void add(final int index, final T element) {
101         throw new UnsupportedOperationException("#add(int, T)");
102     }
103 
104     @Override
105     public boolean addAll(final Collection<? extends T> elements) {
106         throw new UnsupportedOperationException("#addAll(Collection)");
107     }
108 
109     @Override
110     public boolean addAll(final int index, final Collection<? extends T> elms) {
111         throw new UnsupportedOperationException("#add(int, Collection)");
112     }
113 
114     @Override
115     public void clear() {
116         throw new UnsupportedOperationException("#clear()");
117     }
118 
119     @Override
120     public boolean contains(final Object element) {
121         return this.original.contains(element);
122     }
123 
124     @Override
125     public boolean containsAll(final Collection<?> elements) {
126         return this.original.containsAll(elements);
127     }
128 
129     @Override
130     public T get(final int index) {
131         if (index >= this.size()) {
132             throw new ListWrapper.NodeNotFoundException(
133                 String.format(
134                     "Index (%d) is out of bounds (size=%d)",
135                     index, this.size()
136                 ),
137                 this.dom,
138                 this.xpath
139             );
140         }
141         return this.original.get(index);
142     }
143 
144     @Override
145     public int indexOf(final Object element) {
146         return this.original.indexOf(element);
147     }
148 
149     @Override
150     public boolean isEmpty() {
151         return this.original.isEmpty();
152     }
153 
154     @Override
155     public Iterator<T> iterator() {
156         return this.original.iterator();
157     }
158 
159     @Override
160     public int lastIndexOf(final Object element) {
161         return this.original.lastIndexOf(element);
162     }
163 
164     @Override
165     public ListIterator<T> listIterator() {
166         return this.original.listIterator();
167     }
168 
169     @Override
170     public ListIterator<T> listIterator(final int index) {
171         return this.original.listIterator(index);
172     }
173 
174     @Override
175     public boolean equals(final Object other) {
176         return this.original.equals(other);
177     }
178 
179     @Override
180     public int hashCode() {
181         return this.original.hashCode();
182     }
183 
184     @Override
185     public T remove(final int index) {
186         throw new UnsupportedOperationException("#remove(int)");
187     }
188 
189     @Override
190     public boolean remove(final Object element) {
191         throw new UnsupportedOperationException("#remove(Object)");
192     }
193 
194     @Override
195     public boolean removeAll(final Collection<?> elements) {
196         throw new UnsupportedOperationException("#removeAll(Collection)");
197     }
198 
199     @Override
200     public boolean retainAll(final Collection<?> elements) {
201         throw new UnsupportedOperationException("#retainAll(Collection)");
202     }
203 
204     @Override
205     public T set(final int index, final T element) {
206         throw new UnsupportedOperationException("#set(int, T)");
207     }
208 
209     @Override
210     public int size() {
211         return this.original.size();
212     }
213 
214     @Override
215     public List<T> subList(final int start, final int end) {
216         if (start >= this.size()) {
217             throw new ListWrapper.NodeNotFoundException(
218                 String.format(
219                     "Start of subList (%d) is out of bounds (size=%d)",
220                     start, this.size()
221                 ),
222                 this.dom,
223                 this.xpath
224             );
225         }
226         if (end >= this.size()) {
227             throw new ListWrapper.NodeNotFoundException(
228                 String.format(
229                     "End of subList (%d) is out of bounds (size=%d)",
230                     end, this.size()
231                 ),
232                 this.dom,
233                 this.xpath
234             );
235         }
236         return this.original.subList(start, end);
237     }
238 
239     @Override
240     public Object[] toArray() {
241         return this.original.toArray();
242     }
243 
244     @Override
245     @SuppressWarnings("PMD.UseVarargs")
246     public <E> E[] toArray(final E[] array) {
247         return this.original.toArray(array);
248     }
249 
250     /**
251      * Node not found in XmlDocument.
252      *
253      * @since 0.1
254      */
255     private static final class NodeNotFoundException
256         extends IndexOutOfBoundsException {
257         /**
258          * Serialization marker.
259          */
260         private static final long serialVersionUID = 0x7526FA78EEDAC470L;
261 
262         /**
263          * Public ctor.
264          * @param message Error message
265          * @param node The XML with error
266          * @param query The query in XPath
267          */
268         NodeNotFoundException(final String message, final Node node,
269             final CharSequence query) {
270             super(
271                 Logger.format(
272                     "XPath '%s' not found in '%[text]s': %s",
273                     ListWrapper.NodeNotFoundException.escapeUnicode(query),
274                     ListWrapper.NodeNotFoundException.escapeUnicode(
275                         new XMLDocument(node).toString()
276                 ),
277                     message
278             )
279             );
280         }
281 
282         /**
283          * Escape unicode characters.
284          * @param input Input string
285          * @return Escaped output
286          */
287         private static String escapeUnicode(final CharSequence input) {
288             final int length = input.length();
289             final StringBuilder output = new StringBuilder(length);
290             for (int index = 0; index < length; index += 1) {
291                 final char character = input.charAt(index);
292                 if (character < 32 || character > 0x7f) {
293                     output.append(String.format("\\u%X", (int) character));
294                 } else {
295                     output.append(character);
296                 }
297             }
298             return output.toString();
299         }
300     }
301 
302 }