001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau;
014
015import static org.apache.juneau.UriRelativity.*;
016import static org.apache.juneau.UriResolution.*;
017import static org.apache.juneau.common.internal.StringUtils.*;
018import static org.apache.juneau.common.internal.ThrowableUtils.*;
019
020import java.io.*;
021import java.net.*;
022
023/**
024 * Class used to create absolute and root-relative URIs based on your current URI 'location' and rules about how to
025 * make such resolutions.
026 *
027 * <p>
028 * Combines a {@link UriContext} instance with rules for resolution ({@link UriResolution} and relativity
029 * ({@link UriRelativity}) to define simple {@link #resolve(Object)} and {@link #append(Appendable, Object)} methods.
030 *
031 * <p>
032 * Three special protocols are used to represent context-root-relative, servlet-relative, and request-path-relative
033 * URIs:
034 *    <js>"context:/"</js>, <js>"servlet:/"</js>, and <js>"request:/"</js>.
035 *
036 * <p>
037 * The following list shows the protocols of URLs that can be resolved with this class:
038 * <ul>
039 *    <li><js>"foo://foo"</js> - Absolute URI.
040 *    <li><js>"/foo"</js> - Root-relative URI.
041 *    <li><js>"/"</js> - Root URI.
042 *    <li><js>"context:/foo"</js> - Context-root-relative URI.
043 *    <li><js>"context:/"</js> - Context-root URI.
044 *    <li><js>"servlet:/foo"</js> - Servlet-path-relative URI.
045 *    <li><js>"servlet:/"</js> - Servlet-path URI.
046 *    <li><js>"request:/foo"</js> - Request-path-relative URI.
047 *    <li><js>"request:/"</js> - Request-path URI.
048 *    <li><js>"foo"</js> - Path-info-relative URI.
049 *    <li><js>""</js> - Path-info URI.
050 * </ul>
051 *
052 * <h5 class='section'>See Also:</h5><ul>
053 * </ul>
054 */
055public class UriResolver {
056
057   private final UriResolution resolution;
058   private final UriRelativity relativity;
059   private final String authority, contextRoot, servletPath, pathInfo, parentPath;
060
061   /**
062    * Static creator.
063    *
064    * @param resolution Rule on how URIs should be resolved.
065    * @param relativity Rule on what relative URIs are relative to.
066    * @param uriContext Current URI context (i.e. the current URI 'location').
067    * @return A new {@link UriResolver} object.
068    */
069   public static UriResolver of(UriResolution resolution, UriRelativity relativity, UriContext uriContext) {
070      return new UriResolver(resolution, relativity, uriContext);
071   }
072
073   /**
074    * Constructor.
075    *
076    * @param resolution Rule on how URIs should be resolved.
077    * @param relativity Rule on what relative URIs are relative to.
078    * @param uriContext Current URI context (i.e. the current URI 'location').
079    */
080   public UriResolver(UriResolution resolution, UriRelativity relativity, UriContext uriContext) {
081      this.resolution = resolution;
082      this.relativity = relativity;
083      this.authority = uriContext.authority;
084      this.contextRoot = uriContext.contextRoot;
085      this.servletPath = uriContext.servletPath;
086      this.pathInfo = uriContext.pathInfo;
087      this.parentPath = uriContext.parentPath;
088   }
089
090   /**
091    * Converts the specified URI to absolute form based on values in this context.
092    *
093    * @param uri
094    *    The URI to convert to absolute form.
095    *    Can be any of the following:
096    *    <ul>
097    *       <li>{@link java.net.URI}
098    *       <li>{@link java.net.URL}
099    *       <li>{@link CharSequence}
100    *    </ul>
101    *    URI can be any of the following forms:
102    *    <ul>
103    *       <li><js>"foo://foo"</js> - Absolute URI.
104    *       <li><js>"/foo"</js> - Root-relative URI.
105    *       <li><js>"/"</js> - Root URI.
106    *       <li><js>"context:/foo"</js> - Context-root-relative URI.
107    *       <li><js>"context:/"</js> - Context-root URI.
108    *       <li><js>"servlet:/foo"</js> - Servlet-path-relative URI.
109    *       <li><js>"servlet:/"</js> - Servlet-path URI.
110    *       <li><js>"request:/foo"</js> - Request-path-relative URI.
111    *       <li><js>"request:/"</js> - Request-path URI.
112    *       <li><js>"foo"</js> - Path-info-relative URI.
113    *       <li><js>""</js> - Path-info URI.
114    *    </ul>
115    * @return The converted URI.
116    */
117   public String resolve(Object uri) {
118      return resolve(uri, resolution);
119   }
120
121   private String resolve(Object uri, UriResolution res) {
122      String s = stringify(uri);
123      if (isAbsoluteUri(s))
124         return hasDotSegments(s) && res != NONE ? normalize(s) : s;
125      if (res == ROOT_RELATIVE && startsWith(s, '/'))
126         return hasDotSegments(s) ? normalize(s) : s;
127      if (res == NONE && ! isSpecialUri(s))
128         return s;
129      return append(new StringBuilder(), s).toString();
130   }
131
132   /**
133    * Relativizes a URI.
134    *
135    * <p>
136    * Similar to {@link URI#relativize(URI)}, except supports special protocols (e.g. <js>"servlet:/"</js>) for both
137    * the <c>relativeTo</c> and <c>uri</c> parameters.
138    *
139    * <p>
140    * For example, to relativize a URI to its servlet-relative form:
141    * <p class='bjava'>
142    *    <jc>// relativeUri == "path/foo"</jc>
143    *    String <jv>relativeUri</jv> = <jv>resolver</jv>.relativize(<js>"servlet:/"</js>, <js>"/context/servlet/path/foo"</js>);
144    * </p>
145    *
146    * @param relativeTo The URI to relativize against.
147    * @param uri The URI to relativize.
148    * @return The relativized URI.
149    */
150   public String relativize(Object relativeTo, Object uri) {
151      String r = resolve(relativeTo, ABSOLUTE);
152      String s = resolve(uri, ABSOLUTE);
153      return URI.create(r).relativize(URI.create(s)).toString();
154   }
155
156   /**
157    * Same as {@link #resolve(Object)} except appends result to the specified appendable.
158    *
159    * @param a The appendable to append the URL to.
160    * @param o The URI to convert to absolute form.
161    * @return The same appendable passed in.
162    */
163   public Appendable append(Appendable a, Object o) {
164
165      try {
166         String uri = stringify(o);
167         uri = nullIfEmpty(uri);
168         boolean needsNormalize = hasDotSegments(uri) && resolution != null;
169
170         // Absolute paths are not changed.
171         if (isAbsoluteUri(uri))
172            return a.append(needsNormalize ? normalize(uri) : uri);
173         if (resolution == NONE && ! isSpecialUri(uri))
174            return a.append(emptyIfNull(uri));
175         if (resolution == ROOT_RELATIVE && startsWith(uri, '/'))
176            return a.append(needsNormalize ? normalize(uri) : uri);
177
178         Appendable a2 = needsNormalize ? new StringBuilder() : a;
179
180         // Root-relative path
181         if (startsWith(uri, '/')) {
182            if (authority != null)
183               a2.append(authority);
184            if (uri.length() != 1)
185               a2.append(uri);
186            else if (authority == null)
187               a2.append('/');
188         }
189
190         // Context-relative path
191         else if (uri != null && uri.startsWith("context:/")) {
192            if (resolution == ABSOLUTE && authority != null)
193               a2.append(authority);
194            if (contextRoot != null)
195               a2.append('/').append(contextRoot);
196            if (uri.length() > 9)
197               a2.append('/').append(uri.substring(9));
198            else if (contextRoot == null && (authority == null || resolution != ABSOLUTE))
199               a2.append('/');
200         }
201
202         // Resource-relative path
203         else if (uri != null && uri.startsWith("servlet:/")) {
204            if (resolution == ABSOLUTE && authority != null)
205               a2.append(authority);
206            if (contextRoot != null)
207               a2.append('/').append(contextRoot);
208            if (servletPath != null)
209               a2.append('/').append(servletPath);
210            if (uri.length() > 9)
211               a2.append('/').append(uri.substring(9));
212            else if (servletPath == null && contextRoot == null && (authority == null || resolution != ABSOLUTE))
213               a2.append('/');
214         }
215
216         // Request-relative path
217         else if (uri != null && uri.startsWith("request:/")) {
218            if (resolution == ABSOLUTE && authority != null)
219               a2.append(authority);
220            if (contextRoot != null)
221               a2.append('/').append(contextRoot);
222            if (servletPath != null)
223               a2.append('/').append(servletPath);
224            if (pathInfo != null)
225               a2.append('/').append(pathInfo);
226            if (uri.length() > 9)
227               a2.append('/').append(uri.substring(9));
228            else if (servletPath == null && contextRoot == null && pathInfo == null && (authority == null || resolution != ABSOLUTE))
229               a2.append('/');
230         }
231
232         // Relative path
233         else {
234            if (resolution == ABSOLUTE && authority != null)
235               a2.append(authority);
236            if (contextRoot != null)
237               a2.append('/').append(contextRoot);
238            if (servletPath != null)
239               a2.append('/').append(servletPath);
240            if (relativity == RESOURCE && uri != null)
241               a2.append('/').append(uri);
242            else if (relativity == PATH_INFO) {
243               if (uri == null) {
244                  if (pathInfo != null)
245                     a2.append('/').append(pathInfo);
246               } else {
247                  if (parentPath != null)
248                     a2.append('/').append(parentPath);
249                  a2.append('/').append(uri);
250               }
251            }
252            else if (uri == null && contextRoot == null && servletPath == null && (authority == null || resolution != ABSOLUTE))
253               a2.append('/');
254         }
255
256         if (needsNormalize)
257            a.append(normalize(a2.toString()));
258
259         return a;
260      } catch (IOException e) {
261         throw asRuntimeException(e);
262      }
263   }
264
265   private static boolean isSpecialUri(String s) {
266      if (s == null || s.isEmpty())
267         return false;
268      char c = s.charAt(0);
269      if (c != 's' && c != 'c' && c != 'r')
270         return false;
271      return s.startsWith("servlet:/") || s.startsWith("context:/") || s.startsWith("request:/");
272   }
273
274   private static String normalize(String s) {
275      s = URI.create(s).normalize().toString();
276      if (s.length() > 1 && s.charAt(s.length()-1) == '/')
277         s = s.substring(0, s.length()-1);
278      return s;
279   }
280
281   private static boolean hasDotSegments(String s) {
282      if (s == null)
283         return false;
284      for (int i = 0; i < s.length()-1; i++) {
285         char c = s.charAt(i);
286         if ((i == 0 && c == '/') || (c == '/' && s.charAt(i+1) == '.'))
287            return true;
288      }
289      return false;
290   }
291}