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.rest.mock;
018
019import static java.util.Collections.*;
020import static org.apache.juneau.commons.lang.StateEnum.*;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.Shorts.*;
023import static org.apache.juneau.commons.utils.StringUtils.*;
024import static org.apache.juneau.commons.utils.ThrowableUtils.*;
025
026import java.util.*;
027
028import org.apache.juneau.commons.collections.*;
029import org.apache.juneau.commons.lang.*;
030import org.apache.juneau.rest.server.util.*;
031
032/**
033 * Used to resolve incoming URLS to the various URL artifacts of <l>HttpServletRequest</l>.
034 *
035 * <h5 class='section'>See Also:</h5><ul>
036 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauRestMock">juneau-rest-mock</a>
037 * </ul>
038 */
039@SuppressWarnings({
040   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
041})
042public class MockPathResolver {
043
044   private static String fixSegment(String s, Map<String,Object> pathVars) {
045      s = formatNamed(emptyIfNull(s), pathVars);
046      if (s.isEmpty() || eq(s, "/"))
047         return "";
048      s = trimTrailingSlashes(s);
049      if (s.charAt(0) != '/')
050         s = '/' + s;
051      return s;
052
053   }
054
055   private String uri;
056   private String target;
057   private String contextPath;
058   private String servletPath;
059   private String remainder;
060
061   private String error;
062
063   /**
064    * Constructor.
065    *
066    * @param target
067    *    The target portion of the URL (e.g. <js>"http://localhost"</js>).
068    *    <br>If <jk>null</jk>, <js>"http://localhost"</js> is assumed.
069    * @param contextPath
070    *    The context path of the servlet, or <jk>null</jk> if unknown or doesn't have one.
071    * @param servletPath
072    *    The servlet path of the servlet, or <jk>null</jk> if unknown or doesn't have one.
073    * @param pathToResolve
074    *    The path to resolve.
075    *    <br>Can be relative to servlet or an absolute path.
076    * @param pathVars
077    *    Optional path variables to resolve in the context path or servlet path.
078    */
079   public MockPathResolver(String target, String contextPath, String servletPath, String pathToResolve, Map<String,Object> pathVars) {
080      try {
081         init(target, contextPath, servletPath, pathToResolve, pathVars);
082      } catch (Exception e) {
083         error = localizedMessage(e);
084      }
085   }
086
087   /**
088    * Returns the context path of the URL.
089    *
090    * @return The context path of the URL always starting with <js>'/'</js>, or an empty string if it doesn't exist.
091    */
092   public String getContextPath() { return contextPath; }
093
094   /**
095    * Returns any parsing errors.
096    *
097    * @return Any parsing errors, or <jk>null</jk> if parsing was successful.
098    */
099   public String getError() { return error; }
100
101   /**
102    * Returns the remainder of the URL following the context and servlet paths.
103    *
104    * @return The remainder of the URL.
105    */
106   public String getRemainder() { return remainder; }
107
108   /**
109    * Returns the servlet path of the URL.
110    *
111    * @return The servlet path of the URL always starting with <js>'/'</js>, or an empty string if it doesn't exist.
112    */
113   public String getServletPath() { return servletPath; }
114
115   /**
116    * Returns just the hostname portion of the URL.
117    *
118    * @return The hostname portion of the URL.
119    */
120   public String getTarget() { return target; }
121
122   /**
123    * Returns the fully-qualified URL.
124    *
125    * @return The fully-qualified URL.
126    */
127   public String getURI() { return uri; }
128
129   protected FluentMap<String,Object> properties() {
130      // @formatter:off
131      return filteredBeanPropertyMap()
132         .a("contextPath", contextPath)
133         .a("error", error)
134         .a("remainder", remainder)
135         .a("servletPath", servletPath)
136         .a("target", target)
137         .a("uri", uri);
138      // @formatter:on
139   }
140
141   @Override
142   public String toString() {
143      return r(properties());
144   }
145
146   @SuppressWarnings({
147      "java:S3776", // Cognitive complexity acceptable for this specific logic
148      "java:S6541" // Single-threaded context; synchronization unnecessary
149   })
150   private void init(String target, String contextPath, String servletPath, String pathToResolve, Map<String,Object> pathVars) {
151
152      target = trimTrailingSlashes(emptyIfNull(target));
153      if (target.isEmpty())
154         target = "http://localhost";
155
156      contextPath = fixSegment(contextPath, pathVars);
157      servletPath = fixSegment(servletPath, pathVars);
158      pathToResolve = emptyIfNull(pathToResolve);
159
160      if (! (pathToResolve.startsWith("http://") || pathToResolve.startsWith("https://"))) {
161         pathToResolve = fixSegment(pathToResolve, emptyMap());
162         this.uri = target + contextPath + servletPath + pathToResolve;
163         this.target = target;
164         this.contextPath = contextPath;
165         this.servletPath = servletPath;
166         this.remainder = pathToResolve;
167         return;
168      }
169
170      // Path starts with http[s]: so we have to parse it to resolve variables.
171      this.uri = pathToResolve;
172
173      // S3 - Found "http://", looking for any character other than '/' (end of target).
174      // S4 - Found  any character, looking for 3rd '/' (end of target).
175      // S5 - Found 3rd '/', resolving contextPath.
176      // S6 - Resolved contextPath, resolving servletPath.
177      // S7 - Resolved servletPath.
178      StateEnum state = S3;
179
180      int cpSegments = countChars(contextPath, '/');
181      int spSegments = countChars(servletPath, '/');
182
183      this.contextPath = "";
184      this.servletPath = "";
185      this.remainder = "";
186
187      int mark = 0;
188      for (var i = uri.indexOf("://") + 3; i < uri.length(); i++) {
189         var c = uri.charAt(i);
190         if (state == S3) {
191            if (c != '/')
192               state = S4;
193            else
194               break;
195         } else if (state == S4) {
196            if (c == '/') {
197               this.target = uri.substring(0, i);
198               state = S5;
199               if (contextPath.isEmpty()) {
200                  state = S6;
201                  if (servletPath.isEmpty()) {
202                     state = S7;
203                  }
204               }
205               mark = i;
206            }
207         } else if (state == S5) {
208            if (c == '/') {
209               cpSegments--;
210               if (cpSegments == 0) {
211                  this.contextPath = uri.substring(mark, i);
212                  mark = i;
213                  state = S6;
214                  if (servletPath.isEmpty()) {
215                     state = S7;
216                  }
217               }
218            }
219         } else if (state == S6 && c == '/') {
220            spSegments--;
221            if (spSegments == 0) {
222               this.servletPath = uri.substring(mark, i);
223               mark = i;
224               state = S7;
225            }
226         }
227      }
228
229      if (state == S4) {
230         this.target = uri;
231      } else if (state == S5) {
232         this.contextPath = uri.substring(mark);
233      } else if (state == S6) {
234         this.servletPath = uri.substring(mark);
235      } else if (state == S7) {
236         this.remainder = uri.substring(mark);
237      } else {
238         throw rex("Invalid URI pattern encountered:  %s", uri);
239      }
240
241      if (! contextPath.isEmpty()) {
242         var p = UrlPathMatcher.of(contextPath);
243         if (p.match(UrlPath.of(this.contextPath)) == null)
244            throw rex("Context path [%s] not found in URI:  %s", contextPath, uri);
245      }
246
247      if (! servletPath.isEmpty()) {
248         var p = UrlPathMatcher.of(servletPath);
249         if (p.match(UrlPath.of(this.servletPath)) == null)
250            throw rex("Servlet path [%s] not found in URI:  %s", servletPath, uri);
251      }
252   }
253}