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