001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall;
018
019import static org.apache.juneau.commons.function.Suppliers.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023import static org.apache.juneau.commons.utils.ThrowableUtils.*;
024
025import java.util.function.*;
026
027import org.apache.juneau.commons.bean.*;
028import org.apache.juneau.commons.collections.*;
029import org.apache.juneau.marshall.json5.*;
030import org.apache.juneau.marshall.parser.*;
031
032/**
033 * Represents a URL broken into authority/context-root/servlet-path/path-info parts.
034 *
035 * <p>
036 * A typical request against a URL takes the following form:
037 * <p class='bcode'>
038 *    http://host:port/context-root/servlet-path/path-info
039 *    |   authority   |  context   |  resource  |  path  |
040 *    +--------------------------------------------------+
041 * </p>
042 *
043 * <p>
044 * This class allows you to convert URL strings to absolute (e.g. <js>"http://host:port/foo/bar"</js>) or root-relative
045 * (e.g. <js>"/foo/bar"</js>) URLs.
046 *
047 */
048@Marshalled
049@SuppressWarnings({
050   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
051})
052public class UriContext {
053
054   /**
055    * Default URI context.
056    *
057    * <p>
058    * No information about authority, servlet-root, context-root, or path-info is known.
059    */
060   public static final UriContext DEFAULT = new UriContext();
061
062   /**
063    * Static creator.
064    *
065    * @param s
066    *    The input string.
067    *    <br>Example: <js>{authority:'http://localhost:10000',contextRoot:'/myContext',servletPath:'/myServlet',pathInfo:'/foo'}</js>
068    *    <br>Can be <jk>null</jk> (equivalent to the default context).
069    * @return A new {@link UriContext} object.
070    */
071   public static UriContext of(String s) {
072      try {
073         return new UriContext(s);
074      } catch (ParseException e) {
075         throw toRex(e);
076      }
077   }
078
079   /**
080    * Static creator.
081    *
082    * @param authority
083    *    The authority portion of URL (e.g. <js>"http://hostname:port"</js>)
084    *    <br>Can be <jk>null</jk> (treated the same as blank).
085    * @param contextRoot
086    *    The context root of the application (e.g. <js>"/context-root"</js>, or <js>"context-root"</js>)
087    *    <br>Can be <jk>null</jk> (treated the same as blank).
088    * @param servletPath
089    *    The servlet path (e.g. <js>"/servlet-path"</js>, or <js>"servlet-path"</js>)
090    *    <br>Can be <jk>null</jk> (treated the same as blank).
091    * @param pathInfo
092    *    The path info (e.g. <js>"/path-info"</js>, or <js>"path-info"</js>)
093    *    <br>Can be <jk>null</jk> (treated the same as blank).
094    * @return A new {@link UriContext} object.
095    */
096   public static UriContext of(String authority, String contextRoot, String servletPath, String pathInfo) {
097      return new UriContext(authority, contextRoot, servletPath, pathInfo);
098   }
099
100   private static String getParent(String uri) {
101      var i = uri.lastIndexOf('/');
102      if (i <= 1)
103         return "/";
104      return uri.substring(0, i);
105   }
106
107   /** The URI authority (host and port). */
108   public final String authority;
109
110   /** The context root path (typically the servlet context path). */
111   public final String contextRoot;
112
113   /** The servlet path. */
114   public final String servletPath;
115
116   /** The path info portion of the URI. */
117   public final String pathInfo;
118
119   /** The parent path (path excluding the last segment). */
120   public final String parentPath;
121
122   // Memoized suppliers.
123   private final Supplier<String> aContextRoot;
124   private final Supplier<String> rContextRoot;
125   private final Supplier<String> aServletPath;
126   private final Supplier<String> rResource;
127   private final Supplier<String> aPathInfo;
128   private final Supplier<String> rPath;
129
130   /**
131    * Default constructor.
132    *
133    * <p>
134    * All <jk>null</jk> values.
135    */
136   public UriContext() {
137      this(null, null, null, null);
138   }
139
140   /**
141    * String constructor.
142    *
143    * <p>
144    * Input string is a JSON object with the following format:
145    * <js>{authority:'xxx',contextRoot:'xxx',servletPath:'xxx',pathInfo:'xxx'}</js>
146    *
147    * @param s
148    *    The input string.
149    *    <br>Example: <js>{authority:'http://localhost:10000',contextRoot:'/myContext',servletPath:'/myServlet',pathInfo:'/foo'}</js>
150    *    <br>Can be <jk>null</jk> (equivalent to the default context).
151    * @throws ParseException
152    *    If input string is not a valid JSON object.
153    */
154   public UriContext(String s) throws ParseException {
155      var m = Json5Map.ofString(s);
156      this.authority = nie(trimSlashes(m.getString("authority")));
157      this.contextRoot = nie(trimSlashes(m.getString("contextRoot")));
158      this.servletPath = nie(trimSlashes(m.getString("servletPath")));
159      this.pathInfo = nie(trimSlashes(m.getString("pathInfo")));
160      this.parentPath = this.pathInfo == null || this.pathInfo.indexOf('/') == -1 ? null : this.pathInfo.substring(0, this.pathInfo.lastIndexOf('/'));
161      this.rContextRoot = memoize(this::findRContextRoot);
162      this.rResource = memoize(this::findRResource);
163      this.rPath = memoize(this::findRPath);
164      this.aContextRoot = memoize(this::findAContextRoot);
165      this.aServletPath = memoize(this::findAServletPath);
166      this.aPathInfo = memoize(this::findAPathInfo);
167   }
168
169   /**
170    * Constructor.
171    *
172    * <p>
173    * Leading and trailing slashes are trimmed of all parameters.
174    *
175    * <p>
176    * Any parameter can be <jk>null</jk>.  Blanks and nulls are equivalent.
177    *
178    * @param authority
179    *    The authority portion of URL (e.g. <js>"http://hostname:port"</js>)
180    * @param contextRoot
181    *    The context root of the application (e.g. <js>"/context-root"</js>, or <js>"context-root"</js>)
182    * @param servletPath
183    *    The servlet path (e.g. <js>"/servlet-path"</js>, or <js>"servlet-path"</js>)
184    * @param pathInfo
185    *    The path info (e.g. <js>"/path-info"</js>, or <js>"path-info"</js>)
186    */
187   @BeanCtor
188   public UriContext(@Name("authority") String authority, @Name("contextRoot") String contextRoot, @Name("servletPath") String servletPath, @Name("pathInfo") String pathInfo) {
189      this.authority = nie(trimSlashes(authority));
190      this.contextRoot = nie(trimSlashes(contextRoot));
191      this.servletPath = nie(trimSlashes(servletPath));
192      this.pathInfo = nie(trimSlashes(pathInfo));
193      this.parentPath = this.pathInfo == null || this.pathInfo.indexOf('/') == -1 ? null : this.pathInfo.substring(0, this.pathInfo.lastIndexOf('/'));
194      this.rContextRoot = memoize(this::findRContextRoot);
195      this.rResource = memoize(this::findRResource);
196      this.rPath = memoize(this::findRPath);
197      this.aContextRoot = memoize(this::findAContextRoot);
198      this.aServletPath = memoize(this::findAServletPath);
199      this.aPathInfo = memoize(this::findAPathInfo);
200   }
201
202   private String findRContextRoot() {
203      return contextRoot == null ? "/" : ('/' + contextRoot);
204   }
205
206   private String findRResource() {
207      // @formatter:off
208      if (contextRoot == null)
209         return (
210            servletPath == null
211            ? "/"
212            : ('/' + servletPath)
213         );
214      return (
215         servletPath == null
216         ? ('/' + contextRoot)
217         : ('/' + contextRoot + '/' + servletPath)
218      );
219      // @formatter:on
220   }
221
222   private String findRPath() {
223      // @formatter:off
224      if (contextRoot == null) {
225         if (servletPath == null)
226            return (
227               pathInfo == null
228               ? "/"
229               : ('/' + pathInfo)
230            );
231         return (
232            pathInfo == null
233            ? ('/' + servletPath)
234            : ('/' + servletPath + '/' + pathInfo)
235         );
236      }
237      if (servletPath == null)
238         return (
239            pathInfo == null
240            ? ('/' + contextRoot)
241            : ('/' + contextRoot + '/' + pathInfo)
242         );
243      return (
244         pathInfo == null
245         ? ('/' + contextRoot + '/' + servletPath)
246         : ('/' + contextRoot + '/' + servletPath + '/' + pathInfo)
247      );
248      // @formatter:on
249   }
250
251   private String findAContextRoot() {
252      // @formatter:off
253      if (authority == null)
254         return rContextRoot.get();
255      return (
256         contextRoot == null
257         ? authority
258         : (authority + '/' + contextRoot)
259      );
260      // @formatter:on
261   }
262
263   private String findAServletPath() {
264      // @formatter:off
265      if (authority == null)
266         return rResource.get();
267      if (contextRoot == null)
268         return (
269            servletPath == null
270            ? authority
271            : authority + '/' + servletPath
272         );
273      return (
274         servletPath == null
275         ? (authority + '/' + contextRoot)
276         : (authority + '/' + contextRoot + '/' + servletPath)
277      );
278      // @formatter:on
279   }
280
281   private String findAPathInfo() {
282      // @formatter:off
283      if (authority == null)
284         return rPath.get();
285      if (contextRoot == null) {
286         if (servletPath == null)
287            return (
288               pathInfo == null
289               ? authority : (authority + '/' + pathInfo)
290            );
291         return (
292            pathInfo == null
293            ? (authority + '/' + servletPath)
294            : (authority + '/' + servletPath + '/' + pathInfo)
295         );
296      }
297      if (servletPath == null)
298         return (
299            pathInfo == null
300            ? authority + '/' + contextRoot
301            : (authority + '/' + contextRoot + '/' + pathInfo)
302         );
303      return (
304         pathInfo == null
305         ? (authority + '/' + contextRoot + '/' + servletPath)
306         : (authority + '/' + contextRoot + '/' + servletPath + '/' + pathInfo)
307      );
308      // @formatter:on
309   }
310
311   /**
312    * Returns the absolute URI of just the authority portion of this URI context.
313    *
314    * <p>
315    * Example:  <js>"http://hostname:port"</js>
316    *
317    * <p>
318    * If the authority is null/empty, returns <js>"/"</js>.
319    *
320    * @return
321    *    The absolute URI of just the authority portion of this URI context.
322    *    Never <jk>null</jk>.
323    */
324   public String getAbsoluteAuthority() { return authority == null ? "/" : authority; }
325
326   /**
327    * Returns the absolute URI of the context-root portion of this URI context.
328    *
329    * <p>
330    * Example:  <js>"http://hostname:port/context-root"</js>
331    *
332    * @return
333    *    The absolute URI of the context-root portion of this URI context.
334    *    Never <jk>null</jk>.
335    */
336   public String getAbsoluteContextRoot() {
337      return aContextRoot.get();
338   }
339
340   /**
341    * Returns the absolute URI of the path portion of this URI context.
342    *
343    * <p>
344    * Example:  <js>"http://hostname:port/context-root/servlet-path/path-info"</js>
345    *
346    * @return
347    *    The absolute URI of the path portion of this URI context.
348    *    Never <jk>null</jk>.
349    */
350   public String getAbsolutePathInfo() {
351      return aPathInfo.get();
352   }
353
354   /**
355    * Returns the parent of the URL returned by {@link #getAbsolutePathInfo()}.
356    *
357    * @return The parent of the URL returned by {@link #getAbsolutePathInfo()}.
358    */
359   public String getAbsolutePathInfoParent() { return getParent(getAbsolutePathInfo()); }
360
361   /**
362    * Returns the absolute URI of the resource portion of this URI context.
363    *
364    * <p>
365    * Example:  <js>"http://hostname:port/context-root/servlet-path"</js>
366    *
367    * @return
368    *    The absolute URI of the resource portion of this URI context.
369    *    Never <jk>null</jk>.
370    */
371   public String getAbsoluteServletPath() {
372      return aServletPath.get();
373   }
374
375   /**
376    * Returns the parent of the URL returned by {@link #getAbsoluteServletPath()}.
377    *
378    * @return The parent of the URL returned by {@link #getAbsoluteServletPath()}.
379    */
380   public String getAbsoluteServletPathParent() { return getParent(getAbsoluteServletPath()); }
381
382   /**
383    * Returns the root-relative URI of the context portion of this URI context.
384    *
385    * <p>
386    * Example:  <js>"/context-root"</js>
387    *
388    * @return
389    *    The root-relative URI of the context portion of this URI context.
390    *    Never <jk>null</jk>.
391    */
392   public String getRootRelativeContextRoot() {
393      return rContextRoot.get();
394   }
395
396   /**
397    * Returns the root-relative URI of the path portion of this URI context.
398    *
399    * <p>
400    * Example:  <js>"/context-root/servlet-path/path-info"</js>
401    *
402    * @return
403    *    The root-relative URI of the path portion of this URI context.
404    *    Never <jk>null</jk>.
405    */
406   public String getRootRelativePathInfo() {
407      return rPath.get();
408   }
409
410   /**
411    * Returns the parent of the URL returned by {@link #getRootRelativePathInfo()}.
412    *
413    * @return The parent of the URL returned by {@link #getRootRelativePathInfo()}.
414    */
415   public String getRootRelativePathInfoParent() { return getParent(getRootRelativePathInfo()); }
416
417   /**
418    * Returns the root-relative URI of the resource portion of this URI context.
419    *
420    * <p>
421    * Example:  <js>"/context-root/servlet-path"</js>
422    *
423    * @return
424    *    The root-relative URI of the resource portion of this URI context.
425    *    Never <jk>null</jk>.
426    */
427   public String getRootRelativeServletPath() {
428      return rResource.get();
429   }
430
431   /**
432    * Returns the parent of the URL returned by {@link #getRootRelativeServletPath()}.
433    *
434    * @return The parent of the URL returned by {@link #getRootRelativeServletPath()}.
435    */
436   public String getRootRelativeServletPathParent() { return getParent(getRootRelativeServletPath()); }
437
438   protected FluentMap<String,Object> properties() {
439      // @formatter:off
440      return filteredBeanPropertyMap()
441         .a("aContextRoot", aContextRoot.get())
442         .a("aPathInfo", aPathInfo.get())
443         .a("aServletPath", aServletPath.get())
444         .a("authority", authority)
445         .a("contextRoot", contextRoot)
446         .a("parentPath", parentPath)
447         .a("pathInfo", pathInfo)
448         .a("rContextRoot", rContextRoot.get())
449         .a("rResource", rResource.get())
450         .a("servletPath", servletPath)
451         .a("rPath", rPath.get());
452      // @formatter:on
453   }
454
455   @Override /* Overridden from Object */
456   public String toString() {
457      return r(properties());
458   }
459}