Java tutorial
/* * Copyright (c) 2017 Public Library of Science * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ package org.ambraproject.wombat.config.site.url; import com.google.common.base.Preconditions; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; import org.ambraproject.wombat.config.site.RequestMappingContext; import org.ambraproject.wombat.config.site.RequestMappingContextDictionary; import org.ambraproject.wombat.config.site.Site; import org.ambraproject.wombat.config.site.SiteSet; import org.ambraproject.wombat.util.ClientEndpoint; import org.ambraproject.wombat.util.UrlParamBuilder; import org.springframework.http.HttpStatus; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A link to a site page. * <p> * An instance of this class encapsulates a path to the linked page and the site to which the linked page belongs. It * depends on an {@code HttpServletRequest} object in order to build an {@code href} value to appear in the * corresponding response. */ public class Link { private final Optional<Site> site; // absent if the path is for a siteless handler private final String path; private final boolean isAbsolute; private Link(Optional<Site> site, String path, boolean isAbsolute) { this.site = Preconditions.checkNotNull(site); this.path = Preconditions.checkNotNull(path); this.isAbsolute = isAbsolute; } /** * Begin building a link to a page within the same site. * * @param localSite the site for both the originating page and the link target */ public static Factory toLocalSite(Site localSite) { return new Factory(Optional.of(localSite), false); } /** * Begin building a link to an absolute address. * <p> * This should be used only if the resulting link will appear in a context outside of a local site, such as in a * downloadable document file. If the link will appear on a site page served by this application, instead use {@link * #toLocalSite} or {@link #toForeignSite} with a correct {@code localSite} argument. * * @param targetSite the site of the link target */ public static Factory toAbsoluteAddress(Site targetSite) { return new Factory(Optional.of(targetSite), true); } /** * Begin building a link to a page on another site. * * @param localSite the site of the originating page * @param foreignSite the site of the link target */ public static Factory toForeignSite(Site localSite, Site foreignSite) { Optional<String> localHostname = localSite.getRequestScheme().getHostName(); Optional<String> foreignHostname = foreignSite.getRequestScheme().getHostName(); final boolean isAbsolute; if (foreignHostname.isPresent()) { isAbsolute = !localHostname.equals(foreignHostname); } else if (!localHostname.isPresent()) { isAbsolute = false; } else { throw new RuntimeException(String.format("" + "Cannot link to a site with no configured hostname (%s) from a site with one (%s; hostname=%s). " + "(Note: This error can be prevented by configuring a hostname either on every site or none.)", foreignSite.getKey(), localSite.getKey(), localHostname.get())); } return new Factory(Optional.of(foreignSite), isAbsolute); } /** * Begin building a link to a siteless handler. The returned factory object will throw exceptions if {@link * Factory#toPattern} is called for a handler that is not siteless, and will silently link to any path with no site * token if {@link Factory#toPath} is called. * * @see org.ambraproject.wombat.config.site.Siteless */ public static Factory toSitelessHandler() { return SITELESS_FACTORY; } private static final Factory SITELESS_FACTORY = new Factory(Optional.empty(), true); /** * Begin building a link to a page on another site. * * @param localSite the site of the originating page * @param foreignJournalKey the journal key of the target site * @param siteSet the global site set */ public static Factory toForeignSite(Site localSite, String foreignJournalKey, SiteSet siteSet) { Site foreignSite = localSite.getTheme().resolveForeignJournalKey(siteSet, foreignJournalKey); return toForeignSite(localSite, foreignSite); } /** * An intermediate builder class. */ public static class Factory { private final Optional<Site> site; // if absent, may link only to siteless handlers private final boolean isAbsolute; private Factory(Optional<Site> site, boolean isAbsolute) { this.site = Preconditions.checkNotNull(site); this.isAbsolute = isAbsolute; } /** * Build a link to a direct path. * * @param path the path to link to */ public Link toPath(String path) { return new Link(site, path, isAbsolute); } /** * Build a link that will hit a specified request handler. * * @param requestMappingContextDictionary the global handler directory * @param handlerName the name of the target request handler * @param variables values to fill in to path variables in the request handler's pattern * @param queryParameters query parameters to add to the end of the URL * @param wildcardValues values to substitute for wildcards in the request handler's pattern */ public Link toPattern(RequestMappingContextDictionary requestMappingContextDictionary, String handlerName, Map<String, ?> variables, Multimap<String, ?> queryParameters, List<?> wildcardValues) { RequestMappingContext mapping = requestMappingContextDictionary.getPattern(handlerName, site.orElse(null)); if (mapping == null) { String message = site.isPresent() ? String.format("No handler with name=\"%s\" exists for site: %s", handlerName, site.get().getKey()) : String.format("No siteless handler with name=\"%s\" exists", handlerName); throw new PatternNotFoundException(message); } final Optional<Site> linkSite; if (mapping.isSiteless()) { linkSite = Optional.empty(); } else if (site.isPresent()) { linkSite = site; } else { throw new IllegalStateException( "Can link only to Siteless handlers with a 'toSitelessHandler' Factory"); } String path = buildPathFromPattern(mapping.getPattern(), linkSite, variables, queryParameters, wildcardValues); return new Link(linkSite, path, isAbsolute); } public PatternBuilder toPattern(RequestMappingContextDictionary requestMappingContextDictionary, String handlerName) { return new PatternBuilder(requestMappingContextDictionary, handlerName); } public class PatternBuilder { private final RequestMappingContextDictionary requestMappingContextDictionary; private final String handlerName; private final Map<String, Object> pathVariables = new LinkedHashMap<>(); private final Multimap<String, Object> queryParameters = LinkedListMultimap.create(); private final List<Object> wildcardValues = new ArrayList<>(); private PatternBuilder(RequestMappingContextDictionary requestMappingContextDictionary, String handlerName) { this.requestMappingContextDictionary = Objects.requireNonNull(requestMappingContextDictionary); this.handlerName = Objects.requireNonNull(handlerName); } public PatternBuilder addPathVariables(Map<String, ?> pathVariables) { this.pathVariables.putAll(pathVariables); return this; } public PatternBuilder addPathVariable(String key, Object value) { this.pathVariables.put(key, value); return this; } public PatternBuilder addQueryParameters(Map<String, ?> queryParameters) { queryParameters.forEach(this.queryParameters::put); return this; } public PatternBuilder addQueryParameters(Multimap<String, ?> queryParameters) { this.queryParameters.putAll(queryParameters); return this; } public PatternBuilder addQueryParameter(String key, Object value) { this.queryParameters.put(key, value); return this; } public PatternBuilder addWildcardValues(List<?> wildcardValues) { this.wildcardValues.addAll(wildcardValues); return this; } public PatternBuilder addWildcardValue(Object wildcardValue) { this.wildcardValues.add(wildcardValue); return this; } public Link build() { return toPattern(requestMappingContextDictionary, handlerName, pathVariables, queryParameters, wildcardValues); } } } public static class PatternNotFoundException extends RuntimeException { private PatternNotFoundException(String message) { super(message); } } // Match path wildcards of one or two asterisks private static final Pattern WILDCARD = Pattern.compile("\\*\\*?"); private static String buildPathFromPattern(String pattern, Optional<Site> site, Map<String, ?> variables, Multimap<String, ?> queryParameters, List<?> wildcardValues) { Preconditions.checkNotNull(site); Preconditions.checkNotNull(variables); Preconditions.checkNotNull(queryParameters); if (site.isPresent() && site.get().getRequestScheme().hasPathToken()) { if (pattern.equals("/*") || pattern.startsWith("/*/")) { pattern = pattern.substring(2); } else { throw new RuntimeException("Pattern is inconsistent with site's request scheme"); } } String path = fillVariables(pattern, variables); path = fillWildcardValues(path, wildcardValues); path = appendQueryParameters(queryParameters, path); return path; } private static String fillVariables(String path, final Map<String, ?> variables) { UriComponentsBuilder builder = ServletUriComponentsBuilder.fromPath(path); UriComponents.UriTemplateVariables uriVariables = (String name) -> { Object value = variables.get(name); if (value == null) { throw new IllegalArgumentException("Missing required parameter " + name); } return value; }; return builder.build().expand(uriVariables).encode().toString(); } private static String fillWildcardValues(String path, List<?> wildcardValues) { Matcher matcher = WILDCARD.matcher(path); boolean hasAtLeastOneWildcard = matcher.find(); if (!hasAtLeastOneWildcard) { if (wildcardValues.isEmpty()) { return path; } else { throw complainAboutNumberOfWildcards(path, wildcardValues); } } StringBuffer filled = new StringBuffer(path.length() * 2); Iterator<?> valueIterator = wildcardValues.iterator(); do { if (!valueIterator.hasNext()) throw complainAboutNumberOfWildcards(path, wildcardValues); String wildcardValue = valueIterator.next().toString(); if (!matcher.group().equals("**") && wildcardValue.contains("/")) { String message = String.format( "Cannot fill a multi-token value into a single-token wildcard. Value: \"%s\" Path: \"%s\"", wildcardValue, path); throw new IllegalArgumentException(message); } matcher.appendReplacement(filled, wildcardValue); } while (matcher.find()); if (valueIterator.hasNext()) throw complainAboutNumberOfWildcards(path, wildcardValues); matcher.appendTail(filled); return filled.toString(); } private static IllegalArgumentException complainAboutNumberOfWildcards(String path, List<?> wildcardValues) { int wildcardCount = 0; Matcher matcher = WILDCARD.matcher(path); while (matcher.find()) { wildcardCount++; } String message = String.format("Path has %d wildcard%s but was supplied %d value%s. Path=\"%s\" Values=%s", wildcardCount, (wildcardCount == 1 ? "" : "s"), wildcardValues.size(), (wildcardValues.size() == 1 ? "" : "s"), path, wildcardValues.toString()); return new IllegalArgumentException(message); } private static String appendQueryParameters(Multimap<String, ?> queryParameters, String path) { if (!queryParameters.isEmpty()) { UrlParamBuilder paramBuilder = UrlParamBuilder.params(); for (Map.Entry<String, ?> paramEntry : queryParameters.entries()) { paramBuilder.add(paramEntry.getKey(), paramEntry.getValue().toString()); } path = path + "?" + paramBuilder.format(); } return path; } /** * Build a Spring view string that which, if returned from a Spring {@code RequestMapping} method, will redirect to * the linked address. * * @param request the originating request of the page from which to link * @return a Spring redirect string */ public RedirectView getRedirect(HttpServletRequest request) { RedirectView redirectView = new RedirectView(get(request)); redirectView.setStatusCode(HttpStatus.MOVED_PERMANENTLY); return redirectView; } /** * Build a link from this object. The returned value may be either an absolute link (full URL) or a relative link * (path beginning with "/") depending on the sites used to set up this object. * <p> * The returned path is suitable as an {@code href} value to be used in the response to the {@code request} argument. * The argument value must resolve to the local site given to set up this object. * * @param request the originating request of the page from which to link * @return a page that links from the originating page to the target page */ public String get(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); if (isAbsolute) { appendPrefix(sb, request); } sb.append(request.getContextPath()).append('/'); Optional<String> pathToken = site.flatMap(s -> s.getRequestScheme().getPathToken()); if (pathToken.isPresent()) { sb.append(pathToken.get()).append('/'); } String path = this.path.startsWith("/") ? this.path.substring(1) : this.path; sb.append(path); return sb.toString(); } private void appendPrefix(StringBuilder sb, HttpServletRequest request) { String protocol = new String("http"); String header = request.getHeader("X-Forwarded-Proto"); if (header != null && header.equals("https")) { protocol = new String("https"); } sb.append(protocol).append("://"); ClientEndpoint clientEndpoint = ClientEndpoint.get(request); Optional<String> targetHostname = site.flatMap(s -> s.getRequestScheme().getHostName()); sb.append(targetHostname.orElse(clientEndpoint.getHostname())); clientEndpoint.getPort().ifPresent(serverPort -> { sb.append(':').append(serverPort); }); } @Override public String toString() { return "Link{" + "site=" + site + ", path='" + path + '\'' + ", isAbsolute=" + isAbsolute + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Link link = (Link) o; if (isAbsolute != link.isAbsolute) return false; if (!path.equals(link.path)) return false; if (!site.equals(link.site)) return false; return true; } @Override public int hashCode() { int result = site.hashCode(); result = 31 * result + path.hashCode(); result = 31 * result + (isAbsolute ? 1 : 0); return result; } }