Package: ResourcePath
ResourcePath
name | instruction | branch | complexity | line | method | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ResourcePath() |
|
|
|
|
|
||||||||||||||||||||
ResourcePath(List) |
|
|
|
|
|
||||||||||||||||||||
ResourcePath(String) |
|
|
|
|
|
||||||||||||||||||||
appendedWith(ResourcePath) |
|
|
|
|
|
||||||||||||||||||||
appendedWith(String) |
|
|
|
|
|
||||||||||||||||||||
asString() |
|
|
|
|
|
||||||||||||||||||||
canEqual(Object) |
|
|
|
|
|
||||||||||||||||||||
equals(Object) |
|
|
|
|
|
||||||||||||||||||||
getExtension() |
|
|
|
|
|
||||||||||||||||||||
getLeading() |
|
|
|
|
|
||||||||||||||||||||
getSegment(int) |
|
|
|
|
|
||||||||||||||||||||
getSegmentCount() |
|
|
|
|
|
||||||||||||||||||||
getTrailing() |
|
|
|
|
|
||||||||||||||||||||
hashCode() |
|
|
|
|
|
||||||||||||||||||||
isEmpty() |
|
|
|
|
|
||||||||||||||||||||
of(List) |
|
|
|
|
|
||||||||||||||||||||
of(String) |
|
|
|
|
|
||||||||||||||||||||
prependedWith(ResourcePath) |
|
|
|
|
|
||||||||||||||||||||
prependedWith(String) |
|
|
|
|
|
||||||||||||||||||||
relativeTo(ResourcePath) |
|
|
|
|
|
||||||||||||||||||||
startsWith(String) |
|
|
|
|
|
||||||||||||||||||||
static {...} |
|
|
|
|
|
||||||||||||||||||||
toString() |
|
|
|
|
|
||||||||||||||||||||
urlDecoded() |
|
|
|
|
|
||||||||||||||||||||
validated(List) |
|
|
|
|
|
||||||||||||||||||||
validated(String) |
|
|
|
|
|
||||||||||||||||||||
withoutLeading() |
|
|
|
|
|
||||||||||||||||||||
withoutTrailing() |
|
|
|
|
|
Coverage
1: /*
2: * #%L
3: * *********************************************************************************************************************
4: *
5: * NorthernWind - lightweight CMS
6: * http://northernwind.tidalwave.it - git clone https://bitbucket.org/tidalwave/northernwind-src.git
7: * %%
8: * Copyright (C) 2011 - 2023 Tidalwave s.a.s. (http://tidalwave.it)
9: * %%
10: * *********************************************************************************************************************
11: *
12: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
13: * the License. You may obtain a copy of the License at
14: *
15: * http://www.apache.org/licenses/LICENSE-2.0
16: *
17: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
18: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
19: * specific language governing permissions and limitations under the License.
20: *
21: * *********************************************************************************************************************
22: *
23: *
24: * *********************************************************************************************************************
25: * #L%
26: */
27: package it.tidalwave.northernwind.core.model;
28:
29: import javax.annotation.Nonnegative;
30: import javax.annotation.Nonnull;
31: import javax.annotation.concurrent.Immutable;
32: import java.util.List;
33: import java.io.Serializable;
34: import it.tidalwave.northernwind.util.UrlEncoding;
35: import lombok.EqualsAndHashCode;
36: import static java.util.Collections.emptyList;
37: import static java.util.stream.Collectors.*;
38: import static it.tidalwave.util.CollectionUtils.concatAll;
39:
40: /***********************************************************************************************************************
41: *
42: * This class encapsulate a path, that is a sequence of segments separated by a "/", and provides methods to manipulate
43: * it.
44: *
45: * @author Fabrizio Giudici
46: *
47: **********************************************************************************************************************/
48:•@Immutable @EqualsAndHashCode
49: public class ResourcePath implements Serializable
50: {
51: private static final long serialVersionUID = 1L;
52:
53: public static final ResourcePath EMPTY = new ResourcePath();
54:
55: @Nonnull
56: /* package */ final List<String> segments;
57:
58: /*******************************************************************************************************************
59: *
60: * Creates an instance out of a string.
61: *
62: * @param path the path as string
63: * @return the path
64: *
65: ******************************************************************************************************************/
66: @Nonnull
67: public static ResourcePath of (@Nonnull final String path)
68: {
69: return new ResourcePath(path);
70: }
71:
72: /*******************************************************************************************************************
73: *
74: * Creates an instance out of a list of segments.
75: *
76: * @param segments the path as a sequence of segments
77: * @return the path
78: *
79: ******************************************************************************************************************/
80: @Nonnull
81: public static ResourcePath of (@Nonnull final List<String> segments)
82: {
83: return new ResourcePath(segments);
84: }
85:
86: /*******************************************************************************************************************
87: *
88: * Creates an empty path, that is "/".
89: *
90: ******************************************************************************************************************/
91: private ResourcePath()
92: {
93: this(emptyList());
94: }
95:
96: /*******************************************************************************************************************
97: *
98: * Creates an instance out of a string.
99: *
100: * @param path the path
101: *
102: ******************************************************************************************************************/
103: private ResourcePath (@Nonnull final String path)
104: {
105:• this(("/".equals(path) || "".equals(path)) ? emptyList() : List.of(validated(path).split("/")));
106: }
107:
108: /*******************************************************************************************************************
109: *
110: * Creates an instance out of a collection of segments.
111: *
112: * @param segments the segments
113: *
114: ******************************************************************************************************************/
115: /* package */ ResourcePath (@Nonnull final List<String> segments)
116: {
117: this.segments = validated(segments);
118: }
119:
120: /*******************************************************************************************************************
121: *
122: * Returns a clone path which is relative to the given path. For instance, if this is "/foo/bar/baz" and path is
123: * "/foo/bar", the returned clone represents "/baz".
124: *
125: * @param path the path to which we're computing the relative position
126: * @return the clone
127: * @throws IllegalArgumentException if path is not a prefix of this
128: *
129: ******************************************************************************************************************/
130: @Nonnull
131: public ResourcePath relativeTo (@Nonnull final ResourcePath path)
132: {
133:• if (!segments.subList(0, path.segments.size()).equals(path.segments))
134: {
135: throw new IllegalArgumentException("The path " + path.asString() + " doesn't start with " + asString());
136: }
137:
138: return ResourcePath.of(segments.subList(path.segments.size(), segments.size()));
139: }
140:
141: /*******************************************************************************************************************
142: *
143: * Returns the leading segment of this path. For instance, if the current object represents "/foo/bar/baz", "foo" is
144: * returned.
145: *
146: * @return the leading segment of this path
147: *
148: ******************************************************************************************************************/
149: @Nonnull
150: public String getLeading()
151: {
152: return segments.get(0);
153: }
154:
155: /*******************************************************************************************************************
156: *
157: * Returns the trailing segment of this path. For instance, if the current object represents "/foo/bar/baz",
158: * "baz" is returned.
159: *
160: * @return the trailing segment of this path
161: *
162: ******************************************************************************************************************/
163: @Nonnull
164: public String getTrailing()
165: {
166: return segments.get(segments.size() - 1);
167: }
168:
169: /*******************************************************************************************************************
170: *
171: * Returns a segment of this path. For instance, if the current object represents "/foo/bar/baz",
172: * {@code getSegment(1)} returns "baz"..
173: *
174: * @param index the index of the segment
175: * @return the segment
176: *
177: ******************************************************************************************************************/
178: @Nonnull
179: public String getSegment (@Nonnegative final int index)
180: {
181: return segments.get(index);
182: }
183:
184: /*******************************************************************************************************************
185: *
186: * Returns the file extension of this path. For instance, if this object represents "/foo/bar/baz.jpg", "jpg" is
187: * returned.
188: *
189: * @return the file extension of this path
190: *
191: ******************************************************************************************************************/
192: @Nonnull
193: public String getExtension()
194: {
195: final var trailing = getTrailing();
196:• return !trailing.contains(".") ? "" : trailing.replaceAll("^.*\\.", "");
197: }
198:
199: /*******************************************************************************************************************
200: *
201: * Returns a clone without the leading segment. For instance, if the current object represents "/foo/bar/baz",
202: * the returned clone represents "/bar/baz".
203: *
204: * @return the clone
205: *
206: ******************************************************************************************************************/
207: @Nonnull
208: public ResourcePath withoutLeading()
209: {
210: return ResourcePath.of(segments.subList(1, segments.size()));
211: }
212:
213: /*******************************************************************************************************************
214: *
215: * Returns a clone without the trailing segment. For instance, if the current object represents "/foo/bar/baz",
216: * the returned clone represents "/foo/bar".
217: *
218: * @return the clone
219: *
220: ******************************************************************************************************************/
221: @Nonnull
222: public ResourcePath withoutTrailing()
223: {
224: return ResourcePath.of(segments.subList(0, segments.size() - 1));
225: }
226:
227: /*******************************************************************************************************************
228: *
229: * Returns {@code true} if the leading segment of this path is the given one.
230: *
231: * @param leadingSegment the expected leading segment
232: * @return {@code true} if this path starts with the given leading segment
233: *
234: ******************************************************************************************************************/
235: public boolean startsWith (@Nonnull final String leadingSegment)
236: {
237:• return !segments.isEmpty() && getLeading().equals(leadingSegment);
238: }
239:
240: /*******************************************************************************************************************
241: *
242: * Returns the count of segments in this path.
243: *
244: * @return the count of segments
245: *
246: ******************************************************************************************************************/
247: @Nonnegative
248: public int getSegmentCount()
249: {
250: return segments.size();
251: }
252:
253: /*******************************************************************************************************************
254: *
255: * Returns {@code true} if this paths is empty.
256: *
257: * @return {@code true} if the path is empty
258: *
259: ******************************************************************************************************************/
260: @Nonnegative
261: public boolean isEmpty()
262: {
263: return segments.isEmpty();
264: }
265:
266: /*******************************************************************************************************************
267: *
268: * Returns a clone with the given prepended path. For instance, if this object represents "/foo/bar/", and
269: * "baz", "bax" are given as argument, the returned clone represents "/baz/bax/foo/bar".
270: *
271: * @param path the path to prepend
272: * @return the clone
273: *
274: ******************************************************************************************************************/
275: @Nonnull
276: public ResourcePath prependedWith (@Nonnull final ResourcePath path)
277: {
278: return ResourcePath.of(concatAll(path.segments, this.segments));
279: }
280:
281: /*******************************************************************************************************************
282: *
283: * Returns a clone with the given prepended path. For instance, if this object represents "/foo/bar/", and
284: * "baz", "bax" are given as argument, the returned clone represents "/baz/bax/foo/bar".
285: *
286: * @param path the path to prepend
287: * @return the clone
288: *
289: ******************************************************************************************************************/
290: @Nonnull
291: public ResourcePath prependedWith (@Nonnull final String path)
292: {
293: return prependedWith(ResourcePath.of(path));
294: }
295:
296: /*******************************************************************************************************************
297: *
298: * Returns a clone with the given appended path. For instance, if this object represents "/foo/bar/", and
299: * "/baz/bax" is given as argument, the returned clone represents "/foo/bar/baz/bax".
300: *
301: * @param path the path to prepend
302: * @return the clone
303: *
304: ******************************************************************************************************************/
305: @Nonnull
306: public ResourcePath appendedWith (@Nonnull final ResourcePath path)
307: {
308: return ResourcePath.of(concatAll(this.segments, path.segments));
309: }
310:
311: /*******************************************************************************************************************
312: *
313: * Returns a clone with the given appended path. For instance, if this object represents "/foo/bar/", and
314: * "baz", "bax" are given as argument, the returned clone represents "/foo/bar/baz/bax".
315: *
316: * @param path the path to prepend
317: * @return the clone
318: *
319: ******************************************************************************************************************/
320: @Nonnull
321: public ResourcePath appendedWith (@Nonnull final String path)
322: {
323: return appendedWith(ResourcePath.of(path));
324: }
325:
326: /*******************************************************************************************************************
327: *
328: * Returns a URL-decoded clone.
329: *
330: * @return the clone
331: *
332: ******************************************************************************************************************/
333: @Nonnull
334: public ResourcePath urlDecoded()
335: {
336: return ResourcePath.of(segments.stream().map(UrlEncoding::decodedUtf8).collect(toList()));
337: }
338:
339: /*******************************************************************************************************************
340: *
341: * Returns the string representation of this path. This representation always starts with a leading "/" and has no
342: * trailing "/". For empty paths "/" is returned.
343: *
344: * @return the string representation
345: *
346: ******************************************************************************************************************/
347: @Nonnull
348: public String asString()
349: {
350: final var string = segments.stream().collect(joining("/", "/", ""));
351:
352: // FIXME: this check is probably redundant now that there are safety tests
353:• if (string.contains("//"))
354: {
355: throw new RuntimeException("Error in stringification: " + string + " - " + this);
356: }
357:
358: return string;
359: }
360:
361: /*******************************************************************************************************************
362: *
363: * {@inheritDoc}
364: *
365: ******************************************************************************************************************/
366: @Override @Nonnull
367: public String toString()
368: {
369: try
370: {
371: return asString();
372: }
373: catch (RuntimeException e)
374: {
375: return segments.toString();
376: }
377: }
378:
379: /*******************************************************************************************************************
380: *
381: *
382: *
383: ******************************************************************************************************************/
384: @Nonnull
385: private static String validated (@Nonnull final String path)
386: {
387:• if (path.startsWith("http:") || path.startsWith("https:"))
388: {
389: throw new IllegalArgumentException("ResourcePath can't hold a URL");
390: }
391:
392:• final var start = path.startsWith("/") ? 1 : 0;
393: return path.substring(start);
394: }
395:
396: /*******************************************************************************************************************
397: *
398: *
399: *
400: ******************************************************************************************************************/
401: @Nonnull
402: private static List<String> validated (@Nonnull final List<String> segments)
403: {
404:• for (final var segment : segments)
405: {
406:• if ("".equals(segment))
407: {
408: throw new IllegalArgumentException("Empty segment in " + segments);
409: }
410:
411:• if (segment.contains("/"))
412: {
413: throw new IllegalArgumentException("Segments cannot contain a slash: " + segments);
414: }
415: }
416:
417: return segments;
418: }
419: }