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.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022import static org.apache.juneau.marshall.UriRelativity.*;
023import static org.apache.juneau.marshall.UriResolution.*;
024
025import java.io.*;
026import java.net.*;
027
028/**
029 * Class used to create absolute and root-relative URIs based on your current URI 'location' and rules about how to
030 * make such resolutions.
031 *
032 * <p>
033 * Combines a {@link UriContext} instance with rules for resolution ({@link UriResolution} and relativity
034 * ({@link UriRelativity}) to define simple {@link #resolve(Object)} and {@link #append(Appendable, Object)} methods.
035 *
036 * <p>
037 * Three special protocols are used to represent context-root-relative, servlet-relative, and request-path-relative
038 * URIs:
039 *    <js>"context:"</js>, <js>"servlet:"</js>, and <js>"request:"</js>.
040 *
041 * <p>
042 * The following list shows the protocols of URLs that can be resolved with this class:
043 * <ul>
044 *    <li><js>"foo://foo"</js> - Absolute URI.
045 *    <li><js>"/foo"</js> - Root-relative URI.
046 *    <li><js>"/"</js> - Root URI.
047 *    <li><js>"context:/foo"</js> - Context-root-relative URI with path.
048 *    <li><js>"context:/"</js> - Context-root URI.
049 *    <li><js>"context:?foo=bar"</js> - Context-root URI with query string.
050 *    <li><js>"servlet:/foo"</js> - Servlet-path-relative URI with path.
051 *    <li><js>"servlet:/"</js> - Servlet-path URI.
052 *    <li><js>"servlet:?foo=bar"</js> - Servlet-path URI with query string.
053 *    <li><js>"request:/foo"</js> - Request-path-relative URI with path.
054 *    <li><js>"request:/"</js> - Request-path URI.
055 *    <li><js>"request:?foo=bar"</js> - Request-path URI with query string.
056 *    <li><js>"foo"</js> - Path-info-relative URI.
057 *    <li><js>""</js> - Path-info URI.
058 * </ul>
059 *
060 */
061public class UriResolver {
062
063   /**
064    * Static creator.
065    *
066    * @param resolution Rule on how URIs should be resolved.
067    * @param relativity Rule on what relative URIs are relative to.
068    * @param uriContext Current URI context (i.e. the current URI 'location'). Must not be <jk>null</jk>.
069    * @return A new {@link UriResolver} object.
070    */
071   public static UriResolver of(UriResolution resolution, UriRelativity relativity, UriContext uriContext) {
072      return new UriResolver(resolution, relativity, uriContext);
073   }
074
075   private static boolean hasDotSegments(String s) {
076      if (s == null)
077         return false;
078      for (var i = 0; i < s.length() - 1; i++) {
079         var c = s.charAt(i);
080         if ((i == 0 && c == '/') || (c == '/' && s.charAt(i + 1) == '.'))
081            return true;
082      }
083      return false;
084   }
085
086   private static boolean isSpecialUri(String s) {
087      if (isEmpty(s))
088         return false;
089      var c = s.charAt(0);
090      if (c != 's' && c != 'c' && c != 'r')
091         return false;
092      return s.startsWith("servlet:") || s.startsWith("context:") || s.startsWith("request:");
093   }
094
095   private static String normalize(String s) {
096      s = URI.create(s).normalize().toString();
097      if (s.length() > 1 && s.charAt(s.length() - 1) == '/')
098         s = s.substring(0, s.length() - 1);
099      return s;
100   }
101
102   private final UriResolution resolution;
103
104   private final UriRelativity relativity;
105
106   private final String authority;
107   private final String contextRoot;
108   private final String servletPath;
109   private final String pathInfo;
110   private final String parentPath;
111
112   /**
113    * Constructor.
114    *
115    * @param resolution Rule on how URIs should be resolved.
116    * @param relativity Rule on what relative URIs are relative to.
117    * @param uriContext Current URI context (i.e. the current URI 'location'). Must not be <jk>null</jk>.
118    */
119   public UriResolver(UriResolution resolution, UriRelativity relativity, UriContext uriContext) {
120      this.resolution = resolution;
121      this.relativity = relativity;
122      this.authority = uriContext.authority;
123      this.contextRoot = uriContext.contextRoot;
124      this.servletPath = uriContext.servletPath;
125      this.pathInfo = uriContext.pathInfo;
126      this.parentPath = uriContext.parentPath;
127   }
128
129   /**
130    * Same as {@link #resolve(Object)} except appends result to the specified appendable.
131    *
132    * @param a The appendable to append the URL to. Must not be <jk>null</jk>.
133    * @param o The URI to convert to absolute form. Can be <jk>null</jk>.  The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}.
134    * @return The same appendable passed in.
135    */
136   @SuppressWarnings({
137      "java:S3776", // Cognitive complexity acceptable for URI resolution logic
138      "java:S6541" // Brain Method acceptable for URI resolution state machine
139   })
140   public Appendable append(Appendable a, Object o) {
141
142      try {
143         var uri = s(o);
144         uri = nie(uri);
145         var needsNormalize = hasDotSegments(uri) && nn(resolution);
146
147         // Absolute paths are not changed.
148         if (isAbsoluteUri(uri))
149            return a.append(needsNormalize ? normalize(uri) : uri);
150         if (resolution == NONE && ! isSpecialUri(uri))
151            return a.append(emptyIfNull(uri));
152         if (resolution == ROOT_RELATIVE && startsWith(uri, '/'))
153            return a.append(needsNormalize ? normalize(uri) : uri);
154
155         var a2 = needsNormalize ? new StringBuilder() : a;
156
157         // Root-relative path
158         if (startsWith(uri, '/')) {
159            if (nn(authority))
160               a2.append(authority);
161            if (uri.length() != 1)
162               a2.append(uri);
163            else if (authority == null)
164               a2.append('/');
165         }
166
167         // Context-relative path
168         else if (nn(uri) && uri.startsWith("context:")) {
169            if (resolution == ABSOLUTE && nn(authority))
170               a2.append(authority);
171            var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
172            if (hasContext)
173               a2.append('/').append(contextRoot);
174            if (uri.length() > 8) {
175               var remainder = uri.substring(8);
176               // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
177               if (eq(remainder, "/") && (hasContext || (resolution == ABSOLUTE && nn(authority)))) {
178                  // Do nothing
179               } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
180                  a2.append('/').append(remainder);
181               } else {
182                  a2.append(remainder);
183               }
184            } else if (! hasContext && (authority == null || resolution != ABSOLUTE))
185               a2.append('/');
186         }
187
188         // Resource-relative path
189         else if (nn(uri) && uri.startsWith("servlet:")) {
190            if (resolution == ABSOLUTE && nn(authority))
191               a2.append(authority);
192            var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
193            var hasServlet = nn(servletPath) && ! servletPath.isEmpty();
194            if (hasContext)
195               a2.append('/').append(contextRoot);
196            if (hasServlet)
197               a2.append('/').append(servletPath);
198            if (uri.length() > 8) {
199               var remainder = uri.substring(8);
200               // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
201               if (eq(remainder, "/") && (hasContext || hasServlet || (resolution == ABSOLUTE && nn(authority)))) {
202                  // Do nothing
203               } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
204                  a2.append('/').append(remainder);
205               } else {
206                  a2.append(remainder);
207               }
208            } else if (! hasServlet && ! hasContext && (authority == null || resolution != ABSOLUTE))
209               a2.append('/');
210         }
211
212         // Request-relative path
213         else if (nn(uri) && uri.startsWith("request:")) {
214            if (resolution == ABSOLUTE && nn(authority))
215               a2.append(authority);
216            var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
217            var hasServlet = nn(servletPath) && ! servletPath.isEmpty();
218            var hasPath = nn(pathInfo) && ! pathInfo.isEmpty();
219            if (hasContext)
220               a2.append('/').append(contextRoot);
221            if (hasServlet)
222               a2.append('/').append(servletPath);
223            if (hasPath)
224               a2.append('/').append(pathInfo);
225            if (uri.length() > 8) {
226               var remainder = uri.substring(8);
227               // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
228               if (eq(remainder, "/") && (hasContext || hasServlet || hasPath || (resolution == ABSOLUTE && nn(authority)))) {
229                  // Do nothing
230               } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
231                  a2.append('/').append(remainder);
232               } else {
233                  a2.append(remainder);
234               }
235            } else if (! hasServlet && ! hasContext && ! hasPath && (authority == null || resolution != ABSOLUTE))
236               a2.append('/');
237         }
238
239         // Relative path
240         else {
241            if (resolution == ABSOLUTE && nn(authority))
242               a2.append(authority);
243            if (nn(contextRoot))
244               a2.append('/').append(contextRoot);
245            if (nn(servletPath))
246               a2.append('/').append(servletPath);
247            if (relativity == RESOURCE && nn(uri))
248               a2.append('/').append(uri);
249            else if (relativity == PATH_INFO) {
250               if (uri == null) {
251                  if (nn(pathInfo))
252                     a2.append('/').append(pathInfo);
253               } else {
254                  if (nn(parentPath))
255                     a2.append('/').append(parentPath);
256                  a2.append('/').append(uri);
257               }
258            } else if (uri == null && contextRoot == null && servletPath == null && (authority == null || resolution != ABSOLUTE))
259               a2.append('/');
260         }
261
262         if (needsNormalize)
263            a.append(normalize(a2.toString()));
264
265         return a;
266      } catch (IOException e) {
267         throw toRex(e);
268      }
269   }
270
271   /**
272    * Relativizes a URI.
273    *
274    * <p>
275    * Similar to {@link URI#relativize(URI)}, except supports special protocols (e.g. <js>"servlet:/"</js>) for both
276    * the <c>relativeTo</c> and <c>uri</c> parameters.
277    *
278    * <p>
279    * For example, to relativize a URI to its servlet-relative form:
280    * <p class='bjava'>
281    *    <jc>// relativeUri == "path/foo"</jc>
282    *    String <jv>relativeUri</jv> = <jv>resolver</jv>.relativize(<js>"servlet:/"</js>, <js>"/context/servlet/path/foo"</js>);
283    * </p>
284    *
285    * @param relativeTo The URI to relativize against. Can be <jk>null</jk>.  The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}.
286    * @param uri The URI to relativize. Can be <jk>null</jk>.  The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}.
287    * @return The relativized URI.
288    */
289   public String relativize(Object relativeTo, Object uri) {
290      var r = resolve(relativeTo, ABSOLUTE);
291      var s = resolve(uri, ABSOLUTE);
292      return URI.create(r).relativize(URI.create(s)).toString();
293   }
294
295   /**
296    * Converts the specified URI to absolute form based on values in this context.
297    *
298    * @param uri
299    *    The URI to convert to absolute form.
300    *    Can be <jk>null</jk>.  The effect is implementation-dependent, varying with the configured {@link UriResolution} and {@link UriRelativity}.
301    *    Can be any of the following:
302    *    <ul>
303    *       <li>{@link URI}
304    *       <li>{@link URL}
305    *       <li>{@link CharSequence}
306    *    </ul>
307    *    URI can be any of the following forms:
308    *    <ul>
309    *       <li><js>"foo://foo"</js> - Absolute URI.
310    *       <li><js>"/foo"</js> - Root-relative URI.
311    *       <li><js>"/"</js> - Root URI.
312    *       <li><js>"context:/foo"</js> - Context-root-relative URI with path.
313    *       <li><js>"context:/"</js> - Context-root URI.
314    *       <li><js>"context:?foo=bar"</js> - Context-root URI with query string.
315    *       <li><js>"servlet:/foo"</js> - Servlet-path-relative URI with path.
316    *       <li><js>"servlet:/"</js> - Servlet-path URI.
317    *       <li><js>"servlet:?foo=bar"</js> - Servlet-path URI with query string.
318    *       <li><js>"request:/foo"</js> - Request-path-relative URI with path.
319    *       <li><js>"request:/"</js> - Request-path URI.
320    *       <li><js>"request:?foo=bar"</js> - Request-path URI with query string.
321    *       <li><js>"foo"</js> - Path-info-relative URI.
322    *       <li><js>""</js> - Path-info URI.
323    *    </ul>
324    * @return The converted URI.
325    */
326   public String resolve(Object uri) {
327      return resolve(uri, resolution);
328   }
329
330   private String resolve(Object uri, UriResolution res) {
331      var s = s(uri);
332      if (isAbsoluteUri(s))
333         return hasDotSegments(s) && res != NONE ? normalize(s) : s;
334      if (res == ROOT_RELATIVE && startsWith(s, '/'))
335         return hasDotSegments(s) ? normalize(s) : s;
336      if (res == NONE && ! isSpecialUri(s))
337         return s;
338      return append(new StringBuilder(), s).toString();
339   }
340}