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}