View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.handler.url.impl;
21  
22  import java.util.Set;
23  
24  import org.apache.commons.lang3.ObjectUtils;
25  import org.apache.commons.lang3.StringUtils;
26  import org.apache.sling.api.SlingHttpServletRequest;
27  import org.apache.sling.api.adapter.Adaptable;
28  import org.apache.sling.api.resource.Resource;
29  import org.apache.sling.api.resource.ResourceResolver;
30  import org.apache.sling.models.annotations.Model;
31  import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
32  import org.apache.sling.models.annotations.injectorspecific.OSGiService;
33  import org.apache.sling.models.annotations.injectorspecific.Self;
34  import org.apache.sling.models.annotations.injectorspecific.SlingObject;
35  import org.jetbrains.annotations.NotNull;
36  import org.jetbrains.annotations.Nullable;
37  
38  import com.day.cq.wcm.api.Page;
39  
40  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
41  import io.wcm.handler.url.UrlBuilder;
42  import io.wcm.handler.url.UrlHandler;
43  import io.wcm.handler.url.UrlMode;
44  import io.wcm.handler.url.VanityMode;
45  import io.wcm.handler.url.impl.clientlib.ClientlibProxyRewriter;
46  import io.wcm.handler.url.spi.UrlHandlerConfig;
47  import io.wcm.sling.commons.request.RequestParam;
48  import io.wcm.sling.models.annotations.AemObject;
49  import io.wcm.wcm.commons.instancetype.InstanceTypeService;
50  import io.wcm.wcm.commons.util.Path;
51  
52  /**
53   * Default implementation of a {@link UrlHandler}
54   */
55  @Model(adaptables = {
56      SlingHttpServletRequest.class, Resource.class
57  }, adapters = UrlHandler.class)
58  public final class UrlHandlerImpl implements UrlHandler {
59  
60    @Self
61    private Adaptable self;
62    @Self
63    private UrlHandlerConfig urlHandlerConfig;
64    @SlingObject
65    private ResourceResolver resolver;
66    @OSGiService
67    private InstanceTypeService instanceTypeService;
68    @OSGiService
69    private ClientlibProxyRewriter clientlibProxyRewriter;
70  
71    // optional injections (only available if called inside a request)
72    @SlingObject(injectionStrategy = InjectionStrategy.OPTIONAL)
73    private SlingHttpServletRequest request;
74    @AemObject(injectionStrategy = InjectionStrategy.OPTIONAL)
75    private Page currentPage;
76  
77    @Override
78    public @NotNull UrlBuilder get(@NotNull String path) {
79      return new UrlBuilderImpl(path, this);
80    }
81  
82    @Override
83    public @NotNull UrlBuilder get(@NotNull Resource resource) {
84      return new UrlBuilderImpl(resource, this);
85    }
86  
87    @Override
88    public @NotNull UrlBuilder get(@NotNull Page page) {
89      return new UrlBuilderImpl(page, this);
90    }
91  
92    @Override
93    @SuppressWarnings({ "null", "unused" })
94    @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
95    public String rewritePathToContext(@NotNull final Resource resource) {
96      if (resource == null) {
97        return null;
98      }
99      if (currentPage != null) {
100       return rewritePathToContext(resource, currentPage.adaptTo(Resource.class));
101     }
102     else {
103       return resource.getPath();
104     }
105   }
106 
107   @Override
108   @SuppressWarnings({ "null", "unused" })
109   public String rewritePathToContext(@NotNull final Resource resource, @NotNull final Resource contextResource) {
110     if (resource == null) {
111       return null;
112     }
113     if (contextResource == null) {
114       return resource.getPath();
115     }
116 
117     // split up paths
118     String[] contextPathParts = StringUtils.split(Path.getOriginalPath(contextResource.getPath(), resolver), "/");
119     String[] pathParts = StringUtils.split(Path.getOriginalPath(resource.getPath(), resolver), "/");
120 
121     // check if both paths are valid - return unchanged path if not
122     int siteRootLevelContextPath = urlHandlerConfig.getSiteRootLevel(contextResource);
123     int siteRootLevelPath = urlHandlerConfig.getSiteRootLevel(resource);
124     if ((contextPathParts.length <= siteRootLevelContextPath)
125         || (pathParts.length <= siteRootLevelPath)
126         || !StringUtils.equals(contextPathParts[0], "content")
127         || !StringUtils.equals(pathParts[0], "content")) {
128       return resource.getPath();
129     }
130 
131     // rewrite path to current context
132     StringBuilder rewrittenPath = new StringBuilder();
133     for (int i = 0; i <= siteRootLevelContextPath; i++) {
134       rewrittenPath.append('/').append(contextPathParts[i]);
135     }
136     for (int i = siteRootLevelPath + 1; i < pathParts.length; i++) {
137       rewrittenPath.append('/').append(pathParts[i]);
138     }
139     return rewrittenPath.toString();
140   }
141 
142   @Override
143   public boolean isExternalized(@NotNull String url) {
144     return Externalizer.isExternalized(url);
145   }
146 
147   String externalizeLinkUrl(final String url, final Page targetPage, final UrlMode urlMode) {
148 
149     // check for empty url
150     if (StringUtils.isEmpty(url)) {
151       return null;
152     }
153 
154     // do not externalize urls again that are already externalized
155     if (Externalizer.isExternalized(url)) {
156       return url;
157     }
158 
159     String externalizedUrl;
160     if (urlHandlerConfig.isHostProvidedBySlingMapping()) {
161       // apply sling mapping with host
162       externalizedUrl = Externalizer.externalizeUrlWithHost(url, resolver, request);
163     }
164     else {
165       // apply sling mapping, namespace mangling and add webapp context path if required
166       externalizedUrl = Externalizer.externalizeUrl(url, resolver, request);
167     }
168     if (externalizedUrl != null && !Externalizer.isExternalized(externalizedUrl)) {
169       // add link URL prefix (scheme/hostname or integrator placeholder) if required
170       String linkUrlPrefix = getLinkUrlPrefix(urlMode, targetPage);
171       externalizedUrl = StringUtils.defaultString(linkUrlPrefix) + externalizedUrl; //NOPMD
172     }
173     return externalizedUrl;
174   }
175 
176   @SuppressWarnings("null")
177   private String getLinkUrlPrefix(UrlMode urlMode, Page targetPage) {
178     UrlMode mode = ObjectUtils.defaultIfNull(urlMode, urlHandlerConfig.getDefaultUrlMode());
179     String configuredUrlPrefix = mode.getLinkUrlPrefix(self, instanceTypeService.getRunModes(), currentPage, targetPage);
180     return UrlPrefix.applyAutoDetection(configuredUrlPrefix, self);
181   }
182 
183   String externalizeResourceUrl(final String url, final Resource targetResource, final UrlMode urlMode) {
184 
185     // check for empty path
186     if (StringUtils.isEmpty(url)) {
187       return null;
188     }
189 
190     // do not externalize URLs again that are already externalized
191     // do not externalize URLs that do not start with "/" (they are no content paths in that case)
192     if (Externalizer.isExternalized(url) || !Externalizer.isExternalizable(url)) {
193       return url;
194     }
195 
196     // try to resolve the target resource from url/path if it was not given initially (only below /content)
197     Resource resource = targetResource;
198     if (resource == null && StringUtils.startsWith(url, "/content/")) {
199       resource = resolver.resolve(url); // accept NonExistingResource as well
200     }
201 
202     // check for reference to static resource from proxied client library
203     String externalizedUrl = clientlibProxyRewriter.rewriteStaticResourcePath(url);
204 
205     if (urlHandlerConfig.isHostProvidedBySlingMapping()) {
206       // apply sling mapping with host
207       externalizedUrl = Externalizer.externalizeUrlWithHost(externalizedUrl, resolver, request);
208     }
209     else {
210       // apply sling mapping when externalizing URLs
211       externalizedUrl = Externalizer.externalizeUrl(externalizedUrl, resolver, request);
212     }
213     if (externalizedUrl != null && !Externalizer.isExternalized(externalizedUrl)) {
214       // add resource URL prefix (scheme/hostname or integrator placeholder) if required
215       String resourceUrlPrefix = getResourceUrlPrefix(urlMode, resource);
216       externalizedUrl = StringUtils.defaultString(resourceUrlPrefix) + externalizedUrl; //NOPMD
217     }
218     return externalizedUrl;
219   }
220 
221   @SuppressWarnings("null")
222   private String getResourceUrlPrefix(UrlMode urlMode, Resource targetResource) {
223     UrlMode mode = ObjectUtils.defaultIfNull(urlMode, urlHandlerConfig.getDefaultUrlMode());
224     String configuredUrlPrefix = mode.getResourceUrlPrefix(self, instanceTypeService.getRunModes(), currentPage, targetResource);
225     return UrlPrefix.applyAutoDetection(configuredUrlPrefix, self);
226   }
227 
228   String buildUrl(String path, String selector, String extension, String suffix, boolean disableSuffixSelector) { //NOPMD
229     if (StringUtils.isBlank(path)) {
230       return null;
231     }
232 
233     // Extension url part
234     StringBuilder extensionPart = new StringBuilder();
235     if (StringUtils.isNotBlank(extension)) {
236       extensionPart.append('.').append(extension);
237     }
238 
239     // Selector url part
240     StringBuilder selectorPart = new StringBuilder();
241     if (StringUtils.isNotBlank(selector)) {
242       // prepend delimiter to selector if required
243       if (!StringUtils.startsWith(selector, ".")) {
244         selectorPart.append('.');
245       }
246       selectorPart.append(selector);
247     }
248 
249     // Suffix part
250     StringBuilder suffixPart = new StringBuilder();
251     if (StringUtils.isNotBlank(suffix)) {
252       // prepend delimiter to suffix if required and add extension
253       if (!StringUtils.startsWith(suffix, "/")) {
254         suffixPart = suffixPart.append("/");
255       }
256       suffixPart.append(suffix);
257 
258       // if suffix does not contain a file extension add main file extension
259       if (!StringUtils.contains(suffix, ".")) {
260         suffixPart.append(extensionPart);
261       }
262 
263       // add a ".suffix" selector to avoid overlapping of filenames between suffixed and non-suffixed versions of the same page in the dispatcher cache
264       if (!disableSuffixSelector) {
265         selectorPart.append('.').append(UrlHandler.SELECTOR_SUFFIX);
266       }
267     }
268 
269     // build externalized url
270     return path + selectorPart.toString() + extensionPart.toString() + suffixPart.toString();
271   }
272 
273   @SuppressWarnings("java:S3776") // ignore complexity
274   String appendQueryString(String url, String queryString, Set<String> inheritableParameterNames) {
275     if (StringUtils.isEmpty(url)) {
276       return url;
277     }
278 
279     // split url from existing query parameters
280     StringBuilder urlBuilder = new StringBuilder();
281     StringBuilder queryParams = new StringBuilder();
282     int separatorPos = url.indexOf('?');
283     if (separatorPos >= 0) {
284       queryParams.append(url.substring(separatorPos + 1));
285       urlBuilder.append(url.substring(0, separatorPos));
286     }
287     else {
288       urlBuilder.append(url);
289     }
290 
291     // append new query parameters
292     if (StringUtils.isNotBlank(queryString)) {
293       if (queryParams.length() > 0) {
294         queryParams.append('&');
295       }
296       queryParams.append(queryString);
297     }
298 
299     // inherit query parameters from current request (only if the parameter is not already included in the params list)
300     if (inheritableParameterNames != null && request != null) {
301       for (String parameterName : inheritableParameterNames) {
302         if (queryParams.indexOf(parameterName + "=") == -1) {
303           String[] values = RequestParam.getMultiple(request, parameterName);
304           if (values != null) {
305             for (String value : values) {
306               if (StringUtils.isNotEmpty(value)) {
307                 if (queryParams.length() > 0) {
308                   queryParams.append('&');
309                 }
310                 queryParams.append(parameterName);
311                 queryParams.append('=');
312                 queryParams.append(value);
313               }
314             }
315           }
316         }
317       }
318     }
319 
320     // build complete url
321     if (queryParams.length() > 0) {
322       urlBuilder.append('?');
323       urlBuilder.append(queryParams);
324     }
325     return urlBuilder.toString();
326   }
327 
328   String setFragment(String url, String fragment) {
329     if (StringUtils.isEmpty(url)) {
330       return url;
331     }
332 
333     // strip off anchor if already present
334     StringBuilder urlBuilder;
335     int index = url.indexOf('#');
336     if (index >= 0) {
337       urlBuilder = new StringBuilder(url.substring(0, index));
338     }
339     else {
340       urlBuilder = new StringBuilder(url);
341     }
342 
343     // prepend "#" for anchor if not present
344     if (StringUtils.isNotBlank(fragment)) {
345       if (!StringUtils.startsWith(fragment, "#")) {
346         urlBuilder.append('#');
347       }
348       urlBuilder.append(fragment);
349     }
350 
351     return urlBuilder.toString();
352   }
353 
354   @Override
355   public String applySiteUrlAutoDetection(@Nullable String siteUrl) {
356     return UrlPrefix.applyAutoDetection(siteUrl, self);
357   }
358 
359   VanityMode getDefaultVanityMode() {
360     return urlHandlerConfig.getDefaultVanityMode();
361   }
362 
363 }