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;
018
019import static org.apache.juneau.UriRelativity.*;
020import static org.apache.juneau.UriResolution.*;
021import static org.apache.juneau.commons.utils.StringUtils.*;
022import static org.apache.juneau.commons.utils.ThrowableUtils.*;
023import static org.apache.juneau.commons.utils.Utils.*;
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').
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 (s == null || s.isEmpty())
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, contextRoot, servletPath, pathInfo, parentPath;
107
108   /**
109    * Constructor.
110    *
111    * @param resolution Rule on how URIs should be resolved.
112    * @param relativity Rule on what relative URIs are relative to.
113    * @param uriContext Current URI context (i.e. the current URI 'location').
114    */
115   public UriResolver(UriResolution resolution, UriRelativity relativity, UriContext uriContext) {
116      this.resolution = resolution;
117      this.relativity = relativity;
118      this.authority = uriContext.authority;
119      this.contextRoot = uriContext.contextRoot;
120      this.servletPath = uriContext.servletPath;
121      this.pathInfo = uriContext.pathInfo;
122      this.parentPath = uriContext.parentPath;
123   }
124
125   /**
126    * Same as {@link #resolve(Object)} except appends result to the specified appendable.
127    *
128    * @param a The appendable to append the URL to.
129    * @param o The URI to convert to absolute form.
130    * @return The same appendable passed in.
131    */
132   public Appendable append(Appendable a, Object o) {
133
134      try {
135         var uri = s(o);
136         uri = nullIfEmpty(uri);
137         var needsNormalize = hasDotSegments(uri) && nn(resolution);
138
139         // Absolute paths are not changed.
140         if (isAbsoluteUri(uri))
141            return a.append(needsNormalize ? normalize(uri) : uri);
142         if (resolution == NONE && ! isSpecialUri(uri))
143            return a.append(emptyIfNull(uri));
144         if (resolution == ROOT_RELATIVE && startsWith(uri, '/'))
145            return a.append(needsNormalize ? normalize(uri) : uri);
146
147         var a2 = needsNormalize ? new StringBuilder() : a;
148
149         // Root-relative path
150         if (startsWith(uri, '/')) {
151            if (nn(authority))
152               a2.append(authority);
153            if (uri.length() != 1)
154               a2.append(uri);
155            else if (authority == null)
156               a2.append('/');
157         }
158
159         // Context-relative path
160         else if (nn(uri) && uri.startsWith("context:")) {
161            if (resolution == ABSOLUTE && nn(authority))
162               a2.append(authority);
163            var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
164            if (hasContext)
165               a2.append('/').append(contextRoot);
166            if (uri.length() > 8) {
167               var remainder = uri.substring(8);
168               // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
169               if (remainder.equals("/") && (hasContext || (resolution == ABSOLUTE && nn(authority)))) {
170                  // Do nothing
171               } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
172                  a2.append('/').append(remainder);
173               } else {
174                  a2.append(remainder);
175               }
176            } else if (! hasContext && (authority == null || resolution != ABSOLUTE))
177               a2.append('/');
178         }
179
180         // Resource-relative path
181         else if (nn(uri) && uri.startsWith("servlet:")) {
182            if (resolution == ABSOLUTE && nn(authority))
183               a2.append(authority);
184            var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
185            var hasServlet = nn(servletPath) && ! servletPath.isEmpty();
186            if (hasContext)
187               a2.append('/').append(contextRoot);
188            if (hasServlet)
189               a2.append('/').append(servletPath);
190            if (uri.length() > 8) {
191               var remainder = uri.substring(8);
192               // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
193               if (remainder.equals("/") && (hasContext || hasServlet || (resolution == ABSOLUTE && nn(authority)))) {
194                  // Do nothing
195               } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
196                  a2.append('/').append(remainder);
197               } else {
198                  a2.append(remainder);
199               }
200            } else if (! hasServlet && ! hasContext && (authority == null || resolution != ABSOLUTE))
201               a2.append('/');
202         }
203
204         // Request-relative path
205         else if (nn(uri) && uri.startsWith("request:")) {
206            if (resolution == ABSOLUTE && nn(authority))
207               a2.append(authority);
208            var hasContext = nn(contextRoot) && ! contextRoot.isEmpty();
209            var hasServlet = nn(servletPath) && ! servletPath.isEmpty();
210            var hasPath = nn(pathInfo) && ! pathInfo.isEmpty();
211            if (hasContext)
212               a2.append('/').append(contextRoot);
213            if (hasServlet)
214               a2.append('/').append(servletPath);
215            if (hasPath)
216               a2.append('/').append(pathInfo);
217            if (uri.length() > 8) {
218               var remainder = uri.substring(8);
219               // Skip if remainder is just "/" and something was appended OR we're at authority level with nothing else
220               if (remainder.equals("/") && (hasContext || hasServlet || hasPath || (resolution == ABSOLUTE && nn(authority)))) {
221                  // Do nothing
222               } else if (! remainder.isEmpty() && remainder.charAt(0) != '/' && remainder.charAt(0) != '?' && remainder.charAt(0) != '#') {
223                  a2.append('/').append(remainder);
224               } else {
225                  a2.append(remainder);
226               }
227            } else if (! hasServlet && ! hasContext && ! hasPath && (authority == null || resolution != ABSOLUTE))
228               a2.append('/');
229         }
230
231         // Relative path
232         else {
233            if (resolution == ABSOLUTE && nn(authority))
234               a2.append(authority);
235            if (nn(contextRoot))
236               a2.append('/').append(contextRoot);
237            if (nn(servletPath))
238               a2.append('/').append(servletPath);
239            if (relativity == RESOURCE && nn(uri))
240               a2.append('/').append(uri);
241            else if (relativity == PATH_INFO) {
242               if (uri == null) {
243                  if (nn(pathInfo))
244                     a2.append('/').append(pathInfo);
245               } else {
246                  if (nn(parentPath))
247                     a2.append('/').append(parentPath);
248                  a2.append('/').append(uri);
249               }
250            } else if (uri == null && contextRoot == null && servletPath == null && (authority == null || resolution != ABSOLUTE))
251               a2.append('/');
252         }
253
254         if (needsNormalize)
255            a.append(normalize(a2.toString()));
256
257         return a;
258      } catch (IOException e) {
259         throw toRex(e);
260      }
261   }
262
263   /**
264    * Relativizes a URI.
265    *
266    * <p>
267    * Similar to {@link URI#relativize(URI)}, except supports special protocols (e.g. <js>"servlet:/"</js>) for both
268    * the <c>relativeTo</c> and <c>uri</c> parameters.
269    *
270    * <p>
271    * For example, to relativize a URI to its servlet-relative form:
272    * <p class='bjava'>
273    *    <jc>// relativeUri == "path/foo"</jc>
274    *    String <jv>relativeUri</jv> = <jv>resolver</jv>.relativize(<js>"servlet:/"</js>, <js>"/context/servlet/path/foo"</js>);
275    * </p>
276    *
277    * @param relativeTo The URI to relativize against.
278    * @param uri The URI to relativize.
279    * @return The relativized URI.
280    */
281   public String relativize(Object relativeTo, Object uri) {
282      var r = resolve(relativeTo, ABSOLUTE);
283      var s = resolve(uri, ABSOLUTE);
284      return URI.create(r).relativize(URI.create(s)).toString();
285   }
286
287   /**
288    * Converts the specified URI to absolute form based on values in this context.
289    *
290    * @param uri
291    *    The URI to convert to absolute form.
292    *    Can be any of the following:
293    *    <ul>
294    *       <li>{@link java.net.URI}
295    *       <li>{@link java.net.URL}
296    *       <li>{@link CharSequence}
297    *    </ul>
298    *    URI can be any of the following forms:
299    *    <ul>
300    *       <li><js>"foo://foo"</js> - Absolute URI.
301    *       <li><js>"/foo"</js> - Root-relative URI.
302    *       <li><js>"/"</js> - Root URI.
303    *       <li><js>"context:/foo"</js> - Context-root-relative URI with path.
304    *       <li><js>"context:/"</js> - Context-root URI.
305    *       <li><js>"context:?foo=bar"</js> - Context-root URI with query string.
306    *       <li><js>"servlet:/foo"</js> - Servlet-path-relative URI with path.
307    *       <li><js>"servlet:/"</js> - Servlet-path URI.
308    *       <li><js>"servlet:?foo=bar"</js> - Servlet-path URI with query string.
309    *       <li><js>"request:/foo"</js> - Request-path-relative URI with path.
310    *       <li><js>"request:/"</js> - Request-path URI.
311    *       <li><js>"request:?foo=bar"</js> - Request-path URI with query string.
312    *       <li><js>"foo"</js> - Path-info-relative URI.
313    *       <li><js>""</js> - Path-info URI.
314    *    </ul>
315    * @return The converted URI.
316    */
317   public String resolve(Object uri) {
318      return resolve(uri, resolution);
319   }
320
321   private String resolve(Object uri, UriResolution res) {
322      var s = s(uri);
323      if (isAbsoluteUri(s))
324         return hasDotSegments(s) && res != NONE ? normalize(s) : s;
325      if (res == ROOT_RELATIVE && startsWith(s, '/'))
326         return hasDotSegments(s) ? normalize(s) : s;
327      if (res == NONE && ! isSpecialUri(s))
328         return s;
329      return append(new StringBuilder(), s).toString();
330   }
331}