package org.webpieces.router.impl; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.webpieces.ctx.api.Current; import org.webpieces.ctx.api.RequestContext; import org.webpieces.ctx.api.RouterRequest; import org.webpieces.router.api.PortConfig; import org.webpieces.router.api.PortConfigCallback; import org.webpieces.router.api.RouterConfig; import org.webpieces.router.api.exceptions.RouteNotFoundException; import org.webpieces.router.api.routing.RouteId; public class ReverseRoutes { //I don't like this solution(this class) all that much but it works for verifying routes in web pages exist with a run of //a special test to find web app errors before deploying it. good enough beats perfect and lookup is still fast private List<RouteMeta> allRoutes = new ArrayList<>(); private Map<RouteId, RouteMeta> routeIdToRoute = new HashMap<>(); private Map<String, RouteMeta> routeNameToRoute = new HashMap<>(); private Set<String> duplicateNames = new HashSet<>(); private Map<String, RouteMeta> classAndNameToRoute = new HashMap<>(); private Set<String> duplicateClassAndNames = new HashSet<>(); private Map<String, RouteMeta> fullClassAndNameToRoute = new HashMap<>(); private Charset urlEncoding; private PortConfigCallback portConfigCallback; private volatile PortConfig ports; public ReverseRoutes(RouterConfig config) { this.portConfigCallback = config.getPortConfigCallback(); this.urlEncoding = config.getUrlEncoding(); if(portConfigCallback == null) { ports = new PortConfig(8080, 8443); } } public void addContentRoute(RouteMeta meta) { allRoutes.add(meta); } public void addRoute(RouteId routeId, RouteMeta meta) { RouteMeta existingRoute = routeIdToRoute.get(routeId); if(existingRoute != null) { throw new IllegalStateException("You cannot use a RouteId twice. routeId="+routeId +" first time="+existingRoute.getRoute().getFullPath()+" second time="+meta.getRoute().getFullPath()); } allRoutes.add(meta); routeIdToRoute.put(routeId, meta); String enumClassName = routeId.getClass().getSimpleName(); String name = routeId.name(); if(routeNameToRoute.containsKey(name)) { duplicateNames.add(name); } routeNameToRoute.put(name, meta); String classAndName = enumClassName+"."+name; if(classAndNameToRoute.containsKey(classAndName)) { duplicateClassAndNames.add(classAndName); } classAndNameToRoute.put(classAndName, meta); String fullClassAndName = routeId.getClass().getName() +"."+name; fullClassAndNameToRoute.put(fullClassAndName, meta); } public void finalSetup() { //remove duplicates from Map... for(String name : duplicateNames) { routeNameToRoute.remove(name); } for(String classAndName : duplicateClassAndNames) { classAndNameToRoute.remove(classAndName); } } public RouteMeta get(RouteId id) { RouteMeta meta = routeIdToRoute.get(id); if(meta == null) throw new IllegalStateException("addRoute method with param route id="+id+" was never called by your application(your RouteModule files), yet this controller is trying to use it"); return meta; } public RouteMeta get(String name) { String[] pieces = name.split("\\."); if(pieces.length == 1) return getByName(name); else if(pieces.length == 2) return getByClassAndName(name); else if(pieces.length > 2) { return getByFullClassAndName(name); } else throw new IllegalStateException("route not found='"+name+"'"); } private RouteMeta getByFullClassAndName(String name) { RouteMeta meta = fullClassAndNameToRoute.get(name); if(meta == null) throw new RouteNotFoundException("route="+name+" not found."); return meta; } private RouteMeta getByClassAndName(String name) { if(duplicateClassAndNames.contains(name)) { Set<RouteId> keySet = routeIdToRoute.keySet(); String routes = ""; for(RouteId id : keySet) { String potentialName = id.getClass().getSimpleName()+"."+id.name(); if(name.equals(potentialName)) routes += "\nroute="+id.getClass().getName()+"."+id.name(); } throw new RouteNotFoundException("There is more than one route matching the class and name. Qualify it with the package like org.web." +name+". These are the conflicting ids which is why you need to be more specific="+routes); } RouteMeta routeMeta = classAndNameToRoute.get(name); if(routeMeta == null) throw new RouteNotFoundException("route="+name+" not found"); return routeMeta; } private RouteMeta getByName(String name) { if(duplicateNames.contains(name)) { Set<RouteId> keySet = routeIdToRoute.keySet(); String routes = ""; for(RouteId id : keySet) { if(name.equals(id.name())) routes += "\nroute="+id.getClass(); } throw new RouteNotFoundException("There is more than one route matching the name. Qualify it with the class like XXXRouteId." +name+". Same names are found in these enum classes="+routes); } RouteMeta routeMeta = routeNameToRoute.get(name); if(routeMeta == null) throw new RouteNotFoundException("route="+name+" not found."); return routeMeta; } @Override public String toString() { return "ReverseRoutes [routeIdToRoute=" + routeIdToRoute + "]"; } public String convertToUrl(String routeId, Map<String, String> args, boolean isValidating) { RouteMeta routeMeta = get(routeId); Route route = routeMeta.getRoute(); String urlPath = route.getFullPath(); List<String> pathParamNames = route.getPathParamNames(); for(String param : pathParamNames) { String val = args.get(param); if(val == null) { String strArgs = ""; for(Entry<String, String> entry : args.entrySet()) { boolean equals = entry.getKey().equals(param); strArgs = " ARG:'"+entry.getKey()+"'='"+entry.getValue()+"' equals="+equals+"\n"; } throw new RouteNotFoundException("missing argument. param="+param+" is required" + " to exist(and cannot be null as well). route="+routeId+" args="+strArgs); } String encodedVal = urlEncode(val); urlPath = urlPath.replace("{"+param+"}", encodedVal); } if(isValidating) return urlPath; RequestContext ctx = Current.getContext(); RouterRequest request = ctx.getRequest(); if(!route.isHttpsRoute() || request.isHttps) return urlPath; //we are rendering an http page with a link to https so need to do special magic String domain = request.domain; if(ports == null) ports = portConfigCallback.fetchPortConfig(); int httpsPort = ports.getHttpsPort(); return "https://"+domain+":"+httpsPort +urlPath; } private String urlEncode(Object value) { try { return URLEncoder.encode(value.toString(), urlEncoding.name()); } catch(UnsupportedEncodingException e) { throw new RuntimeException(e); } } public Collection<RouteMeta> getAllRouteMetas() { return allRoutes; } }