diff --git a/example_application/adapters/config/ApplicationRoutes.java b/example_application/adapters/config/ApplicationRoutes.java index 0402cfd..9d60a9e 100644 --- a/example_application/adapters/config/ApplicationRoutes.java +++ b/example_application/adapters/config/ApplicationRoutes.java @@ -1,7 +1,7 @@ package config; import controllers.PeopleController; -import hex.action.RouteManager; +import hex.action.routing.RouteManager; /** * Created by jason on 14-11-16. diff --git a/example_application/views/layouts/application.html.jsp b/example_application/views/layouts/application.html.jsp index 3d4cf84..7e885d9 100644 --- a/example_application/views/layouts/application.html.jsp +++ b/example_application/views/layouts/application.html.jsp @@ -9,10 +9,12 @@

Hex Example Application

Menu

diff --git a/example_application/views/people/index.html.jsp b/example_application/views/people/index.html.jsp index 211e3bc..26c6ddc 100644 --- a/example_application/views/people/index.html.jsp +++ b/example_application/views/people/index.html.jsp @@ -22,12 +22,10 @@ ${p.firstName} ${p.lastName} - - View + View - -Create New Person +Create New Person diff --git a/hex_action/resources/META-INF/tld/hex.tld b/hex_action/resources/META-INF/tld/hex.tld index 7572968..a5eb1ba 100644 --- a/hex_action/resources/META-INF/tld/hex.tld +++ b/hex_action/resources/META-INF/tld/hex.tld @@ -7,7 +7,7 @@ http://hex.org/tags view-content - hex.action.views.jsp.tags.ViewContentTag + hex.jsp.tags.ViewContentTag empty name @@ -16,7 +16,7 @@ content-for - hex.action.views.jsp.tags.ContentForTag + hex.jsp.tags.ContentForTag JSP name @@ -25,7 +25,53 @@ view-init - hex.action.views.jsp.tags.ViewInitTag + hex.jsp.tags.ViewInitTag empty + + link + hex.jsp.tags.LinkTag + JSP + + action + true + + + anchor + false + + + to + false + true + + + + query-param + hex.jsp.tags.QueryParamTag + empty + + name + true + + + value + true + true + + + + param + hex.jsp.tags.ParamTag + empty + + name + true + + + value + true + true + + diff --git a/hex_action/src/hex/action/Application.java b/hex_action/src/hex/action/Application.java index 7bebff8..81cde32 100644 --- a/hex_action/src/hex/action/Application.java +++ b/hex_action/src/hex/action/Application.java @@ -1,5 +1,6 @@ package hex.action; +import hex.action.routing.RouteManager; import hex.action.initialization.InitializationException; import hex.action.initialization.InitializerRunner; import hex.routing.Routing; @@ -25,6 +26,7 @@ public class Application implements ServletContextListener { public static final String ACTION_CONFIG = "hex_action.properties"; public static final String ROUTES = "routes"; public static final String ROOT = "hex.action.Application.ROOT"; + public static final String ROUTING_CONFIG_CLASSES = "hex.action.Application.ROUTING_CONFIG_CLASSES"; @Override public void contextInitialized(ServletContextEvent sce) { @@ -38,7 +40,7 @@ public void contextInitialized(ServletContextEvent sce) { private void initializeApplication(ServletContext application) throws InitializationException { ClassLoader cl = getClass().getClassLoader(); try { - RoutingConfig routingConfig = initializeRoutes(cl); + RoutingConfig routingConfig = initializeRoutes(cl, application); application.setAttribute(Routing.CONFIG, routingConfig); InitializerRunner initializer = new InitializerRunner(); @@ -64,7 +66,7 @@ public void contextDestroyed(ServletContextEvent sce) { } - public static RoutingConfig initializeRoutes(ClassLoader classLoader) throws ServletException { + public static RoutingConfig initializeRoutes(ClassLoader classLoader, ServletContext application) throws ServletException { try { Properties adapterProperties = new Properties(); adapterProperties.load(classLoader.getResourceAsStream(ACTION_CONFIG)); @@ -72,11 +74,15 @@ public static RoutingConfig initializeRoutes(ClassLoader classLoader) throws Ser String[] routingConfigClasses = Stream.of(adapterProperties.getProperty(ROUTES).split(",")) .map(String::trim).toArray(String[]::new); + application.setAttribute(ROUTING_CONFIG_CLASSES, routingConfigClasses); + RoutingConfigBase routingConfig = new RoutingConfigBase(); for (String className : routingConfigClasses) { RouteManager routeManager = (RouteManager) Class.forName(className, false, classLoader).newInstance(); routeManager.setHexActionProperties(adapterProperties); routeManager.defineRoutes(); + application.setAttribute(className, routeManager); + //TODO: This is smelly. Need to decide if supporting multiple RouteManagers in the same routing config is a good idea or not. routeManager.getDefinedRoutes().forEach(routingConfig::addRoute); } return routingConfig; diff --git a/hex_action/src/hex/action/Controller.java b/hex_action/src/hex/action/Controller.java index ee678ce..4d720b8 100644 --- a/hex_action/src/hex/action/Controller.java +++ b/hex_action/src/hex/action/Controller.java @@ -37,6 +37,10 @@ public class Controller { * @return Name of the directory in which to find the view template. */ protected String templateDirectory() { + return getName(); + } + + String getName() { Matcher m = CONTROLLER_NAMES.matcher(getClass().getSimpleName()); if(!m.matches()) throw new IllegalArgumentException(String.format(Errors.UNABLE_TO_IMPLY_VIEW_DIRECTORY, getClass().getSimpleName())); return underscore(m.replaceAll(m.group(1))); diff --git a/hex_action/src/hex/action/ControllerAction.java b/hex_action/src/hex/action/ControllerAction.java index 50ea702..3b58780 100644 --- a/hex_action/src/hex/action/ControllerAction.java +++ b/hex_action/src/hex/action/ControllerAction.java @@ -94,6 +94,10 @@ public void prepareController(Controller controller, ServletRequest servletReque } } + public String getName() { + return actionName; + } + private void checkException(ServletRequest request) throws ServletException { InitializationException e = (InitializationException) request.getServletContext().getAttribute(InitializationException.class.getName()); diff --git a/hex_action/src/hex/action/RouteManager.java b/hex_action/src/hex/action/RouteManager.java deleted file mode 100644 index d588962..0000000 --- a/hex_action/src/hex/action/RouteManager.java +++ /dev/null @@ -1,63 +0,0 @@ -package hex.action; - -import hex.routing.HttpMethod; -import hex.routing.Route; - -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.function.Supplier; - -/** - * Created by jason on 14-11-14. - */ -public class RouteManager { - private List definedRoutes = new ArrayList<>(); - - private Properties hexActionProperties; - - public Properties getHexActionConfig() { - return hexActionProperties; - } - - public void setHexActionProperties(Properties hexActionProperties) { - this.hexActionProperties = hexActionProperties; - } - - public List getDefinedRoutes() { - return definedRoutes; - } - - public RouteManager() { - } - - public void defineRoutes() { - - } - - public void get(String path, Supplier controllerSupplier, String action) { - definedRoutes.add(new Route(HttpMethod.GET, path, getHandler(controllerSupplier, action))); - } - - public void post(String path, Supplier controllerSupplier, String action) { - definedRoutes.add(new Route(HttpMethod.POST, path, getHandler(controllerSupplier, action))); - } - - public void put(String path, Supplier controllerSupplier, String action) { - definedRoutes.add(new Route(HttpMethod.PUT, path, getHandler(controllerSupplier, action))); - } - - public void delete(String path, Supplier controllerSupplier, String action) { - definedRoutes.add(new Route(HttpMethod.DELETE, path, getHandler(controllerSupplier, action))); - } - - public void matches(String path, Supplier controllerSupplier, String action) { - definedRoutes.add(new Route(HttpMethod.ANY, path, getHandler(controllerSupplier, action))); - } - - private ControllerAction getHandler(Supplier controllerSupplier, String action) { - ControllerAction controllerAction = new ControllerAction(controllerSupplier, action); - controllerAction.setHexActionConfig(getHexActionConfig()); - return controllerAction; - } -} diff --git a/hex_action/src/hex/action/ViewContext.java b/hex_action/src/hex/action/ViewContext.java index c1d90cd..46dbbf0 100644 --- a/hex_action/src/hex/action/ViewContext.java +++ b/hex_action/src/hex/action/ViewContext.java @@ -1,6 +1,6 @@ package hex.action; -import hex.utils.maps.CoercionMap; +import hex.utils.maps.PropertyMap; import java.util.HashMap; import java.util.Map; @@ -8,7 +8,7 @@ /** * Created by jason on 14-11-15. */ -public class ViewContext extends HashMap implements CoercionMap { +public class ViewContext extends HashMap implements PropertyMap { private static final String BLANK = ""; private String content; diff --git a/hex_action/src/hex/action/routing/Errors.java b/hex_action/src/hex/action/routing/Errors.java new file mode 100644 index 0000000..88ba0a5 --- /dev/null +++ b/hex_action/src/hex/action/routing/Errors.java @@ -0,0 +1,9 @@ +package hex.action.routing; + +/** + * Created by jason on 15-01-04. + */ +public class Errors { + static final String ROUTE_NAME_NOT_FOUND = + "Route named (%s) not found in application routes."; +} diff --git a/hex_action/src/hex/action/routing/RouteManager.java b/hex_action/src/hex/action/routing/RouteManager.java new file mode 100644 index 0000000..345f89d --- /dev/null +++ b/hex_action/src/hex/action/routing/RouteManager.java @@ -0,0 +1,91 @@ +package hex.action.routing; + +import hex.action.Controller; +import hex.action.ControllerAction; +import hex.routing.HttpMethod; +import hex.routing.Route; + +import java.util.*; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * Created by jason on 14-11-14. + */ +public class RouteManager { + private final Map routeMap = new HashMap<>(); + + private final List definedRoutes = new ArrayList<>(); + + private Properties hexActionProperties; + + public Properties getHexActionConfig() { + return hexActionProperties; + } + + public void setHexActionProperties(Properties hexActionProperties) { + this.hexActionProperties = hexActionProperties; + } + + public List getDefinedRoutes() { + return definedRoutes; + } + + public RouteManager() { + } + + public void defineRoutes() { + + } + + protected void get(String path, Supplier controllerSupplier, String action) { + addRoute(HttpMethod.GET, path, getHandler(controllerSupplier, action)); + } + + protected void post(String path, Supplier controllerSupplier, String action) { + addRoute(HttpMethod.POST, path, getHandler(controllerSupplier, action)); + } + + protected void put(String path, Supplier controllerSupplier, String action) { + addRoute(HttpMethod.PUT, path, getHandler(controllerSupplier, action)); + } + + protected void delete(String path, Supplier controllerSupplier, String action) { + addRoute(HttpMethod.DELETE, path, getHandler(controllerSupplier, action)); + } + + protected void matches(String path, Supplier controllerSupplier, String action) { + addRoute(HttpMethod.ANY, path, getHandler(controllerSupplier, action)); + } + + private void addRoute(Predicate any, String path, ControllerAction handler) { + Route route = new Route(any, path, handler); + definedRoutes.add(route); + registerRouteName(handler.getName(), path, route); + } + + private void addRoute(HttpMethod method, String path, ControllerAction handler) { + Route route = new Route(method, path, handler); + definedRoutes.add(route); + registerRouteName(handler.getName(), path, route); + } + + private void registerRouteName(String actionName, String path, Route route) { + String pathName = Route.PATH_PARAM_PATTERN.matcher(path).replaceAll("") + .replaceFirst("/", "") + .replaceAll("/", "_"); + String routeName = actionName; + if(pathName.length() > 0) routeName += "_" + pathName; + routeMap.put(routeName, route); + } + + Route getRouteNamed(String routeName) { + return routeMap.get(routeName); + } + + private ControllerAction getHandler(Supplier controllerSupplier, String action) { + ControllerAction controllerAction = new ControllerAction(controllerSupplier, action); + controllerAction.setHexActionConfig(getHexActionConfig()); + return controllerAction; + } +} diff --git a/hex_action/src/hex/action/routing/Uri.java b/hex_action/src/hex/action/routing/Uri.java new file mode 100644 index 0000000..ac9e036 --- /dev/null +++ b/hex_action/src/hex/action/routing/Uri.java @@ -0,0 +1,71 @@ +package hex.action.routing; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** +* Created by jason on 15-01-04. +*/ +public class Uri { + private Optional scheme = Optional.empty(); + + private Optional host = Optional.empty(); + + private int port; + + private Optional context = Optional.empty(); + + private Optional path = Optional.empty(); + + private Optional anchor = Optional.empty(); + + private Map queryParams = new LinkedHashMap<>(); + + public static Uri create(String scheme, String host, int port) { + Uri uri = new Uri(); + uri.scheme = Optional.of(scheme); + uri.host = Optional.of(host); + uri.port = port; + return uri; + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + scheme.ifPresent(s -> builder.append(s).append("://")); + host.ifPresent(builder::append); + if(port != 0 && port != 80) builder.append(':').append(port); + context.ifPresent(builder::append); + path.ifPresent(builder::append); + if(queryParams.size() > 0) { + builder.append('?'); + builder.append(queryParams.entrySet().stream().map(Object::toString).collect(Collectors.joining("&"))); + } + anchor.ifPresent(s -> builder.append('#').append(s)); + return builder.toString(); + } + + public void addQueryParam(String name, String value) { + try { + queryParams.put(name, URLEncoder.encode(value, "UTF-8")); + } catch (UnsupportedEncodingException e) { + // hardcoded UTF-8 + // TODO: Log a warning here, just in case. + } + } + + public void setContext(String context) { + this.context = Optional.ofNullable(context); + } + + public void setPath(String path) { + this.path = Optional.ofNullable(path); + } + + public void setAnchor(String anchorValue) { + this.anchor = Optional.ofNullable(anchorValue); + } +} diff --git a/hex_action/src/hex/action/routing/UriFactory.java b/hex_action/src/hex/action/routing/UriFactory.java new file mode 100644 index 0000000..9102bac --- /dev/null +++ b/hex_action/src/hex/action/routing/UriFactory.java @@ -0,0 +1,48 @@ +package hex.action.routing; + +import hex.action.Application; +import hex.routing.Route; + +import javax.servlet.ServletRequest; +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by jason on 15-01-03. + */ +public class UriFactory { + private static final Pattern PATH_NAME_PATTERN = Pattern.compile("(.*)_(url|path)$"); + + private static final String URL = "url"; + + public static Uri getUrlFor(String pathRef, ServletRequest request, Object values) { + Matcher m = PATH_NAME_PATTERN.matcher(pathRef); + if(!m.matches()) throw new IllegalArgumentException("Path reference did not match format (path_name)_(url|path)"); + String pathName = m.group(1); + String uriType = m.group(2); + Uri uri = getUri(uriType, request); + uri.setContext(request.getServletContext().getContextPath()); + // TODO: add check for Tomcat mode with it's stupid trailing slash in "directory paths" + Route route = getRouteNamed(pathName, request); + uri.setPath(route.createPath(values)); + return uri; + } + + private static Uri getUri(String uriType, ServletRequest request) { + if(URL.equals(uriType)) { + return Uri.create(request.getScheme(), request.getServerName(), request.getServerPort()); + } + return new Uri(); + } + + private static Route getRouteNamed(String pathName, ServletRequest request) { + String[] routingConfigClasses = (String[]) request.getServletContext().getAttribute(Application.ROUTING_CONFIG_CLASSES); + for(String configClass : routingConfigClasses) { + RouteManager routingConfig = (RouteManager) request.getServletContext().getAttribute(configClass); + Route route = routingConfig.getRouteNamed(pathName); + if(route != null) return route; + } + throw new NoSuchElementException(String.format(Errors.ROUTE_NAME_NOT_FOUND, pathName)); + } +} diff --git a/hex_action/src/hex/action/views/html/HtmlElement.java b/hex_action/src/hex/action/views/html/HtmlElement.java new file mode 100644 index 0000000..3d6eaf6 --- /dev/null +++ b/hex_action/src/hex/action/views/html/HtmlElement.java @@ -0,0 +1,84 @@ +package hex.action.views.html; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by jason on 15-01-10. + */ +public class HtmlElement { + private static final char LT = '<'; + private static final char GT = '>'; + private static final char CS = '/'; + private static final char SPACE = ' '; + private static final char EQ = '='; + private static final char QUOTE = '"'; + + private String tagName; + + private String body; + + private final Map attributes = new LinkedHashMap<>(); + + public HtmlElement(String tagName) { + this.tagName = tagName; + } + + public void setBody(String body) { + this.body = body; + } + + public String toString() { + StringWriter out = new StringWriter(); + try { + write(out); + } catch (IOException e) { + // StringWriter won't throw exception + // And if it does.... whatever... + } + return out.toString(); + } + + public HtmlElement setAttribute(String name, String value) { + attributes.put(name, value); + return this; + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + + public String getAttribute(String name) { + return attributes.get(name); + } + + private Writer quote(String value, Writer to) throws IOException { + return to.append(QUOTE).append(value).append(QUOTE); + } + + public void write(Writer out) throws IOException { + List errors = new ArrayList<>(); + out.append(LT).append(tagName); + if(attributes.size() > 0) { + attributes.forEach((k,v) -> { + try { + out.append(SPACE).append(k).append(EQ); + quote(v, out); + } catch (IOException e) { + errors.add(e); + } + }); + if(errors.size() > 0) throw errors.get(0); + } + if(body == null) { + out.append(CS).append(GT); + } else { + out.append(GT).append(body).append(LT).append(CS).append(tagName).append(GT); + } + } +} diff --git a/hex_action/src/hex/action/views/ViewSupport.java b/hex_action/src/hex/jsp/ViewSupport.java similarity index 97% rename from hex_action/src/hex/action/views/ViewSupport.java rename to hex_action/src/hex/jsp/ViewSupport.java index c05fc7d..31d0842 100644 --- a/hex_action/src/hex/action/views/ViewSupport.java +++ b/hex_action/src/hex/jsp/ViewSupport.java @@ -1,4 +1,4 @@ -package hex.action.views; +package hex.jsp; import hex.action.ControllerAction; import hex.action.ViewContext; diff --git a/hex_action/src/hex/action/views/jsp/tags/ContentForTag.java b/hex_action/src/hex/jsp/tags/ContentForTag.java similarity index 95% rename from hex_action/src/hex/action/views/jsp/tags/ContentForTag.java rename to hex_action/src/hex/jsp/tags/ContentForTag.java index 2bfb860..35c90bd 100644 --- a/hex_action/src/hex/action/views/jsp/tags/ContentForTag.java +++ b/hex_action/src/hex/jsp/tags/ContentForTag.java @@ -1,8 +1,8 @@ -package hex.action.views.jsp.tags; +package hex.jsp.tags; import hex.action.ControllerAction; import hex.action.ViewContext; -import hex.action.views.ViewSupport; +import hex.jsp.ViewSupport; import javax.servlet.jsp.JspException; import javax.servlet.jsp.PageContext; diff --git a/hex_action/src/hex/jsp/tags/HexActionTagBase.java b/hex_action/src/hex/jsp/tags/HexActionTagBase.java new file mode 100644 index 0000000..23acec9 --- /dev/null +++ b/hex_action/src/hex/jsp/tags/HexActionTagBase.java @@ -0,0 +1,49 @@ +package hex.jsp.tags; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.BodyTagSupport; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by jason on 15-01-24. + */ +public abstract class HexActionTagBase extends BodyTagSupport implements UriTag { + protected String actionName; + + protected Object values; + + protected Map params; + + private Map queryParams; + + public void setAction(String actionName) { + this.actionName = actionName; + } + + public void setValues(Object values) { + this.values = values; + } + + @Override + public void setQueryParam(String name, String value) { + queryParams.put(name, value); + } + + public void setParam(String name, String value) { + params.put(name, value); + } + + /** + * Default processing of the start tag, returning SKIP_BODY. + * + * @return SKIP_BODY + * @throws javax.servlet.jsp.JspException if an error occurs while processing this tag + */ + @Override + public int doStartTag() throws JspException { + params = new HashMap<>(); + queryParams = new HashMap<>(); + return EVAL_BODY_BUFFERED; + } +} diff --git a/hex_action/src/hex/jsp/tags/LinkTag.java b/hex_action/src/hex/jsp/tags/LinkTag.java new file mode 100644 index 0000000..93e0924 --- /dev/null +++ b/hex_action/src/hex/jsp/tags/LinkTag.java @@ -0,0 +1,45 @@ +package hex.jsp.tags; + +import hex.action.routing.Uri; +import hex.action.routing.UriFactory; +import hex.action.views.html.HtmlElement; + +import javax.servlet.jsp.JspException; +import java.io.IOException; + +/** + * Created by jason on 15-01-03. + */ +public class LinkTag extends HexActionTagBase { + + private String anchor; + + public void setAnchor(String anchor) { + this.anchor = anchor; + } + + public void setTo(Object values) { + setValues(values); + } + + /** + * Default processing of the end tag returning EVAL_PAGE. + * + * @return EVAL_PAGE + * @throws javax.servlet.jsp.JspException if an error occurs while processing this tag + */ + @Override + public int doEndTag() throws JspException { + Uri uri = UriFactory.getUrlFor(actionName, pageContext.getRequest(), + params.size() > 0 ? params : values); + HtmlElement element = new HtmlElement("a"); + element.setAttribute("href", uri.toString()); + element.setBody(getBodyContent().getString()); + try { + element.write(pageContext.getOut()); + } catch (IOException e) { + throw new JspException("Error while writing html element", e); + } + return EVAL_PAGE; + } +} diff --git a/hex_action/src/hex/jsp/tags/ParamTag.java b/hex_action/src/hex/jsp/tags/ParamTag.java new file mode 100644 index 0000000..fb21ae1 --- /dev/null +++ b/hex_action/src/hex/jsp/tags/ParamTag.java @@ -0,0 +1,44 @@ +package hex.jsp.tags; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.tagext.SimpleTagSupport; +import java.io.IOException; +import java.util.Objects; + +/** + * Created by jason on 15-01-13. + */ +public class ParamTag extends SimpleTagSupport { + protected String name; + + protected String value; + + public void setName(String name) { + this.name = name; + } + + public void setValue(String value) { + this.value = value; + } + + /** + * Default processing of the tag does nothing. + * + * @throws javax.servlet.jsp.JspException Subclasses can throw JspException to indicate + * an error occurred while processing this tag. + * @throws javax.servlet.jsp.SkipPageException If the page that + * (either directly or indirectly) invoked this tag is to + * cease evaluation. A Simple Tag Handler generated from a + * tag file must throw this exception if an invoked Classic + * Tag Handler returned SKIP_PAGE or if an invoked Simple + * Tag Handler threw SkipPageException or if an invoked Jsp Fragment + * threw a SkipPageException. + * @throws java.io.IOException Subclasses can throw IOException if there was + */ + @Override + public void doTag() throws JspException, IOException { + UriTag tag = (UriTag) findAncestorWithClass(this, UriTag.class); + Objects.requireNonNull(tag); + tag.setParam(name, value); + } +} diff --git a/hex_action/src/hex/jsp/tags/QueryParamTag.java b/hex_action/src/hex/jsp/tags/QueryParamTag.java new file mode 100644 index 0000000..25f15ca --- /dev/null +++ b/hex_action/src/hex/jsp/tags/QueryParamTag.java @@ -0,0 +1,33 @@ +package hex.jsp.tags; + +import javax.servlet.jsp.JspException; +import java.io.IOException; +import java.util.Objects; + +/** + * Created by jason on 15-01-07. + */ +public class QueryParamTag extends ParamTag { + + /** + * Default processing of the tag does nothing. + * + * @throws javax.servlet.jsp.JspException Subclasses can throw JspException to indicate + * an error occurred while processing this tag. + * @throws javax.servlet.jsp.SkipPageException If the page that + * (either directly or indirectly) invoked this tag is to + * cease evaluation. A Simple Tag Handler generated from a + * tag file must throw this exception if an invoked Classic + * Tag Handler returned SKIP_PAGE or if an invoked Simple + * Tag Handler threw SkipPageException or if an invoked Jsp Fragment + * threw a SkipPageException. + * @throws java.io.IOException Subclasses can throw IOException if there was + * an error writing to the output stream + */ + @Override + public void doTag() throws JspException, IOException { + UriTag tag = (UriTag)findAncestorWithClass(this, UriTag.class); + Objects.requireNonNull(tag); + tag.setQueryParam(name, value); + } +} diff --git a/hex_action/src/hex/jsp/tags/UriTag.java b/hex_action/src/hex/jsp/tags/UriTag.java new file mode 100644 index 0000000..6b95239 --- /dev/null +++ b/hex_action/src/hex/jsp/tags/UriTag.java @@ -0,0 +1,10 @@ +package hex.jsp.tags; + +/** + * Created by jason on 15-01-16. + */ +public interface UriTag { + void setQueryParam(String name, String value); + + void setParam(String name, String value); +} diff --git a/hex_action/src/hex/action/views/jsp/tags/ViewContentTag.java b/hex_action/src/hex/jsp/tags/ViewContentTag.java similarity index 93% rename from hex_action/src/hex/action/views/jsp/tags/ViewContentTag.java rename to hex_action/src/hex/jsp/tags/ViewContentTag.java index 84cc81d..8616c13 100644 --- a/hex_action/src/hex/action/views/jsp/tags/ViewContentTag.java +++ b/hex_action/src/hex/jsp/tags/ViewContentTag.java @@ -1,8 +1,8 @@ -package hex.action.views.jsp.tags; +package hex.jsp.tags; import hex.action.ControllerAction; import hex.action.ViewContext; -import hex.action.views.ViewSupport; +import hex.jsp.ViewSupport; import javax.servlet.jsp.JspException; import javax.servlet.jsp.PageContext; diff --git a/hex_action/src/hex/action/views/jsp/tags/ViewInitTag.java b/hex_action/src/hex/jsp/tags/ViewInitTag.java similarity index 95% rename from hex_action/src/hex/action/views/jsp/tags/ViewInitTag.java rename to hex_action/src/hex/jsp/tags/ViewInitTag.java index 86a0e75..d2707af 100644 --- a/hex_action/src/hex/action/views/jsp/tags/ViewInitTag.java +++ b/hex_action/src/hex/jsp/tags/ViewInitTag.java @@ -1,6 +1,6 @@ -package hex.action.views.jsp.tags; +package hex.jsp.tags; -import hex.action.views.ViewSupport; +import hex.jsp.ViewSupport; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; diff --git a/hex_action/test/hex/action/ActionTests.java b/hex_action/test/hex/action/ActionTests.java index c8b37a4..398f198 100644 --- a/hex_action/test/hex/action/ActionTests.java +++ b/hex_action/test/hex/action/ActionTests.java @@ -1,7 +1,8 @@ package hex.action; import hex.action.params.ParamsSuite; -import hex.action.views.jsp.TagTests; +import hex.action.routing.RoutingSuite; +import hex.action.views.ViewsSuite; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -11,10 +12,10 @@ @RunWith(Suite.class) @Suite.SuiteClasses({ ControllerActionTest.class, - RouteManagerTest.class, + RoutingSuite.class, ControllerTest.class, ParamsSuite.class, - TagTests.class + ViewsSuite.class }) public class ActionTests { } diff --git a/hex_action/test/hex/action/params/WebParamsTest.java b/hex_action/test/hex/action/params/WebParamsTest.java index ba5a81b..f696e08 100644 --- a/hex_action/test/hex/action/params/WebParamsTest.java +++ b/hex_action/test/hex/action/params/WebParamsTest.java @@ -6,7 +6,7 @@ import hex.routing.RouteParams; import hex.utils.Memo; import hex.utils.coercion.CoercionException; -import hex.utils.maps.CoercionMap; +import hex.utils.coercion.CoercionMap; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.Test; diff --git a/hex_action/test/hex/action/RouteManagerTest.java b/hex_action/test/hex/action/routing/RouteManagerTest.java similarity index 62% rename from hex_action/test/hex/action/RouteManagerTest.java rename to hex_action/test/hex/action/routing/RouteManagerTest.java index 62a5666..b4d72b5 100644 --- a/hex_action/test/hex/action/RouteManagerTest.java +++ b/hex_action/test/hex/action/routing/RouteManagerTest.java @@ -1,4 +1,4 @@ -package hex.action; +package hex.action.routing; import hex.action.examples.PostsController; import hex.routing.HttpMethod; @@ -8,8 +8,8 @@ import java.util.stream.Stream; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.*; /** * Created by jason on 14-11-15. @@ -67,4 +67,34 @@ public void matchesShouldMatchAnyOldMethod() { m -> assertTrue(m.toString(), route.matches(m, path)) ); } + + @Test + public void addingRoutesShouldCreatePathNames() { + String path = "/aliens"; + routeManager.get(path, PostsController::new, "report_to_the_mother_ship"); + Route route = routeManager.getRouteNamed("report_to_the_mother_ship_aliens"); + assertThat(route, notNullValue()); + assertTrue(route.matches(HttpMethod.GET, path)); + } + + @Test + public void addingRoutesWithParamsShouldSkipParamsInPathNames() { + routeManager.get("/aliens/:id", PostsController::new, "profile"); + Route route = routeManager.getRouteNamed("profile_aliens"); + assertThat(route, notNullValue()); + } + + @Test + public void addingNestedRoutesShouldNestPathNames() { + routeManager.get("/posts/:postId/comments/:id", PostsController::new, "show"); + Route route = routeManager.getRouteNamed("show_posts_comments"); + assertThat(route, notNullValue()); + } + + @Test + public void routesToRootShouldBeJustTheActionName() { + routeManager.matches("/", PostsController::new, "home"); + Route route = routeManager.getRouteNamed("home"); + assertThat(route, notNullValue()); + } } diff --git a/hex_action/test/hex/action/routing/RoutingSuite.java b/hex_action/test/hex/action/routing/RoutingSuite.java new file mode 100644 index 0000000..51f95d8 --- /dev/null +++ b/hex_action/test/hex/action/routing/RoutingSuite.java @@ -0,0 +1,15 @@ +package hex.action.routing; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * Created by jason on 15-01-04. + */ +@RunWith(Suite.class) @Suite.SuiteClasses({ + RouteManagerTest.class, + UriTest.class +}) +public class RoutingSuite { + +} diff --git a/hex_action/test/hex/action/routing/UriTest.java b/hex_action/test/hex/action/routing/UriTest.java new file mode 100644 index 0000000..22303d2 --- /dev/null +++ b/hex_action/test/hex/action/routing/UriTest.java @@ -0,0 +1,46 @@ +package hex.action.routing; + +import org.junit.Test; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +/** + * Created by jason on 15-01-04. + */ +public class UriTest { + @Test + public void queryParamsShouldBeFormattedCorrectly() { + Uri uri = new Uri(); + uri.setPath("/cowboys"); + uri.addQueryParam("and", "aliens"); + assertThat(uri.toString(), is("/cowboys?and=aliens")); + } + + @Test + public void multipleQueryParamsShouldBeCool() { + Uri uri = new Uri(); + uri.setPath("/posts"); + uri.addQueryParam("tag", "interesting"); + uri.addQueryParam("title", "Atlantis"); + assertThat(uri.toString(), is("/posts?tag=interesting&title=Atlantis")); + } + + @Test + public void queryStringAndAnchor() { + Uri uri = new Uri(); + uri.setPath("/aliens"); + uri.addQueryParam("encounterYear", "1947"); + uri.setAnchor("list"); + assertThat(uri.toString(), is("/aliens?encounterYear=1947#list")); + } + + @Test + public void onceMoreWithFeeling() { + Uri uri = Uri.create("https", "example.com", 3000); + uri.setPath("/roswell"); + uri.addQueryParam("recordYear", "1947"); + uri.setAnchor("conspiracies"); + assertThat(uri.toString(), is("https://example.com:3000/roswell?recordYear=1947#conspiracies")); + } +} diff --git a/hex_action/test/hex/action/views/ViewsSuite.java b/hex_action/test/hex/action/views/ViewsSuite.java new file mode 100644 index 0000000..76a9b9c --- /dev/null +++ b/hex_action/test/hex/action/views/ViewsSuite.java @@ -0,0 +1,16 @@ +package hex.action.views; + +import hex.action.views.html.HtmlElementTest; +import hex.action.views.jsp.TagTests; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +/** + * Created by jason on 15-01-10. + */ +@RunWith(Suite.class) @Suite.SuiteClasses({ + TagTests.class, + HtmlElementTest.class +}) +public class ViewsSuite { +} diff --git a/hex_action/test/hex/action/views/html/HtmlElementTest.java b/hex_action/test/hex/action/views/html/HtmlElementTest.java new file mode 100644 index 0000000..567e2c8 --- /dev/null +++ b/hex_action/test/hex/action/views/html/HtmlElementTest.java @@ -0,0 +1,57 @@ +package hex.action.views.html; + +import org.hamcrest.Matchers; +import org.junit.Test; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +/** + * Created by jason on 15-01-10. + */ +public class HtmlElementTest { + private HtmlElement tag; + + private void setTag(String tagName) { + this.tag = new HtmlElement(tagName); + } + + @Test + public void shouldRenderHtml() { + setTag("a"); + tag.setBody("Home"); + assertThat(tag.toString(), is("Home")); + } + + @Test + public void shouldSelfCloseWhenAppropriate() { + setTag("rara"); + assertThat(tag.toString(), is("")); + } + + @Test + public void shouldIncludeAttributesWithSelfClosing() { + setTag("input"); + tag.setAttribute("type", "text"); + tag.setAttribute("name", "userName"); + String tagHtml = tag.toString(); + assertThat(tagHtml, containsString("")); + } + + @Test + public void shouldIncludeAttributesInBodyTag() { + setTag("form"); + tag.setAttribute("method", "POST"); + tag.setAttribute("action", "/people"); + tag.setBody("some form html"); + String tagHtml = tag.toString(); + assertThat(tagHtml, containsString("some form html<")); + assertThat(tagHtml, containsString("")); + } +} diff --git a/hex_action/test/hex/action/views/jsp/TagTests.java b/hex_action/test/hex/action/views/jsp/TagTests.java index 1d77d9f..b30dcf2 100644 --- a/hex_action/test/hex/action/views/jsp/TagTests.java +++ b/hex_action/test/hex/action/views/jsp/TagTests.java @@ -1,6 +1,6 @@ package hex.action.views.jsp; -import hex.action.views.jsp.tags.ViewContentTagTest; +import hex.jsp.tags.ViewContentTagTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; diff --git a/hex_action/test/hex/jsp/tags/LinkTagTest.java b/hex_action/test/hex/jsp/tags/LinkTagTest.java new file mode 100644 index 0000000..a8c3471 --- /dev/null +++ b/hex_action/test/hex/jsp/tags/LinkTagTest.java @@ -0,0 +1,41 @@ +package hex.jsp.tags; + +import hex.action.Application; +import hex.action.routing.RouteManager; +import org.junit.Before; +import org.junit.Test; +import servlet_mock.jsp.MockJspContext; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Created by jason on 15-01-24. + */ +public class LinkTagTest { + private RouteManager routes = new RouteManager(); + + private LinkTag tag; + + @Before + public void setUpTag() { + tag = new LinkTag(); + } + + @Test + public void linksShouldRenderQueryParams() { + + } + + private void setUpRequest(HttpServletRequest request, HttpServletResponse response) { + setUpRoutes(request.getServletContext()); + tag.setPageContext(new MockJspContext(request)); + + } + + private void setUpRoutes(ServletContext servletContext) { + servletContext.setAttribute(Application.ROUTING_CONFIG_CLASSES, new String[]{ LinkTagTest.class.getName() }); + servletContext.setAttribute(LinkTagTest.class.getName(), routes); + } +} diff --git a/hex_action/test/hex/action/views/jsp/tags/ViewContentTagTest.java b/hex_action/test/hex/jsp/tags/ViewContentTagTest.java similarity index 96% rename from hex_action/test/hex/action/views/jsp/tags/ViewContentTagTest.java rename to hex_action/test/hex/jsp/tags/ViewContentTagTest.java index d7483ce..2871864 100644 --- a/hex_action/test/hex/action/views/jsp/tags/ViewContentTagTest.java +++ b/hex_action/test/hex/jsp/tags/ViewContentTagTest.java @@ -1,4 +1,4 @@ -package hex.action.views.jsp.tags; +package hex.jsp.tags; import hex.action.ControllerAction; import hex.action.ViewContext; @@ -27,8 +27,12 @@ public class ViewContentTagTest { protected MockJspContext jspContext; @Before - public void setUp() { + public void setUpViewContext() { view = new ViewContext(); + } + + @Before + public void setUp() { tag = new ViewContentTag(); view.setContent("This is action view template content"); } diff --git a/hex_dev/src/hex/dev/DevRoutingFilter.java b/hex_dev/src/hex/dev/DevRoutingFilter.java index e3d18e2..a9aaf3d 100644 --- a/hex_dev/src/hex/dev/DevRoutingFilter.java +++ b/hex_dev/src/hex/dev/DevRoutingFilter.java @@ -85,21 +85,18 @@ private void initCompiler(String applicationRootPath) { } private void runApplicationInitializers(FilterConfig filterConfig) throws IOException { - URLClassLoader classLoader = new HexClassLoader(applicationCompiler, this.getClass().getClassLoader()); - InitializerRunner runner = new InitializerRunner(classLoader); - try { + try(URLClassLoader classLoader = new HexClassLoader(applicationCompiler, this.getClass().getClassLoader())) { + InitializerRunner runner = new InitializerRunner(classLoader); runner.run(); } catch (InitializationException e) { filterConfig.getServletContext().setAttribute(InitializationException.class.getName(), e); - } finally { - classLoader.close(); } } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try (URLClassLoader requestClassLoader = new HexClassLoader(applicationCompiler, this.getClass().getClassLoader())) { - config = Application.initializeRoutes(requestClassLoader); + config = Application.initializeRoutes(requestClassLoader, servletRequest.getServletContext()); filter.doFilter(servletRequest, servletResponse, filterChain); } finally { config = null; diff --git a/hex_routing/src/hex/routing/Route.java b/hex_routing/src/hex/routing/Route.java index 7d15e6d..1ae0f40 100644 --- a/hex_routing/src/hex/routing/Route.java +++ b/hex_routing/src/hex/routing/Route.java @@ -23,8 +23,11 @@ */ package hex.routing; +import hex.utils.coercion.CoercionObject; + import javax.servlet.http.HttpServletRequest; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.function.Predicate; import java.util.regex.Matcher; @@ -36,7 +39,12 @@ public class Route { public static final String ROUTE_PARAMS = "hex.hex_routing.Route.ROUTE_PARAMS"; - private static final Pattern PATH_PARAM_DETECTOR = Pattern.compile("/:(\\w+)"); + public static final Pattern PATH_PARAM_PATTERN = Pattern.compile("/:(\\w+)"); + + /** + * The original path used to create this {@link Route} + */ + private String path; private Pattern pathPattern; @@ -52,6 +60,10 @@ public Route(RouteHandler handler) { this(HttpMethod.ANY, handler); } + public Route(String path, RouteHandler handler) { + this(HttpMethod.ANY, path, handler); + } + public Route(HttpMethod method, RouteHandler handler) { this(m -> m == method, handler); } @@ -75,8 +87,9 @@ private void setPattern(Pattern pathPattern) { this.pathPattern = pathPattern; } - public void setPath(String path) { - Matcher m = PATH_PARAM_DETECTOR.matcher(path); + void setPath(String path) { + this.path = path; + Matcher m = PATH_PARAM_PATTERN.matcher(path); while(m.find()) { addParam(m.group(1)); } @@ -87,6 +100,24 @@ public void setPath(String path) { setPattern(Pattern.compile(routePattern + "[/]?")); } + public String createPath() { + return path; + } + + public String createPath(Object params) { + CoercionObject paramValues = new CoercionObject(params); + Matcher m = PATH_PARAM_PATTERN.matcher(path); + StringBuffer path = new StringBuffer(); + + while(m.find()) { + String paramName = m.group(1); + m.appendReplacement(path, "/" + paramValues.getString(paramName)); + } + m.appendTail(path); + + return path.toString(); + } + public RouteHandler getHandler() { return (q, r) -> { HttpServletRequest request = (HttpServletRequest) q; diff --git a/hex_routing/src/hex/routing/RouteParams.java b/hex_routing/src/hex/routing/RouteParams.java index b2f55cd..588789e 100644 --- a/hex_routing/src/hex/routing/RouteParams.java +++ b/hex_routing/src/hex/routing/RouteParams.java @@ -23,7 +23,7 @@ */ package hex.routing; -import hex.utils.maps.CoercionMap; +import hex.utils.coercion.CoercionMap; import hex.utils.maps.AbstractImmutableMap; import java.math.BigDecimal; diff --git a/hex_routing/test/hex/routing/RouteTest.java b/hex_routing/test/hex/routing/RouteTest.java index eec056a..017f8d5 100644 --- a/hex_routing/test/hex/routing/RouteTest.java +++ b/hex_routing/test/hex/routing/RouteTest.java @@ -23,10 +23,14 @@ */ package hex.routing; +import hex.routing.test.Post; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; import java.util.stream.Stream; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; import static servlet_mock.HttpMock.GET; @@ -80,4 +84,29 @@ public void theRootRouteShouldWork() { p -> assertTrue(String.format("%s -> %s", r, p), route.matches(HttpMethod.GET, p))); }); } + + @Test + public void createPathShouldReturnTheDefinedPath() { + Route route = new Route("/aliens", RoutingConfigTest.CALLED); + String path = route.createPath(); + assertThat(path, is("/aliens")); + } + + @Test + public void createPathShouldSubstituteParamsWithPassedInProperties() { + Route route = new Route("/posts/:id", RoutingConfigTest.CALLED); + Post post = new Post(); + post.setId(99); + String path = route.createPath(post); + assertThat(path, is("/posts/99")); + } + + @Test + public void createPathShouldWorkWithMapsAsWell() { + Route route = new Route("/home/:profile", RoutingConfigTest.CALLED); + Map params = new HashMap<>(); + params.put("profile", "a.einstein"); + String path = route.createPath(params); + assertThat(path, is("/home/a.einstein")); + } } diff --git a/hex_routing/test/hex/routing/test/Post.java b/hex_routing/test/hex/routing/test/Post.java new file mode 100644 index 0000000..500ece9 --- /dev/null +++ b/hex_routing/test/hex/routing/test/Post.java @@ -0,0 +1,16 @@ +package hex.routing.test; + +/** + * Created by jason on 15-01-03. + */ +public class Post { + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} diff --git a/hex_routing/test/servlet_mock/jsp/MockJspContext.java b/hex_routing/test/servlet_mock/jsp/MockJspContext.java index 696799f..c25b788 100644 --- a/hex_routing/test/servlet_mock/jsp/MockJspContext.java +++ b/hex_routing/test/servlet_mock/jsp/MockJspContext.java @@ -3,12 +3,15 @@ import servlet_mock.MockHttpServletRequest; import javax.el.ELContext; +import javax.servlet.*; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import javax.servlet.jsp.JspContext; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.PageContext; import javax.servlet.jsp.el.ExpressionEvaluator; import javax.servlet.jsp.el.VariableResolver; +import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @@ -18,7 +21,7 @@ /** * Created by jason on 14-12-23. */ -public class MockJspContext extends JspContext { +public class MockJspContext extends PageContext { private final Map attributes = new HashMap<>(); private final HttpServletRequest request; @@ -312,4 +315,317 @@ public ELContext getELContext() { private IntStream scopeStream() { return IntStream.of(PageContext.PAGE_SCOPE, PageContext.REQUEST_SCOPE, PageContext.SESSION_SCOPE, PageContext.APPLICATION_SCOPE); } + + /** + *

+ * The initialize method is called to initialize an uninitialized PageContext + * so that it may be used by a JSP Implementation class to service an + * incoming request and response within it's _jspService() method. + *

+ *

+ * This method is typically called from JspFactory.getPageContext() in + * order to initialize state. + *

+ *

+ * This method is required to create an initial JspWriter, and associate + * the "out" name in page scope with this newly created object. + *

+ *

+ * This method should not be used by page or tag library authors. + * + * @param servlet The Servlet that is associated with this PageContext + * @param request The currently pending request for this Servlet + * @param response The currently pending response for this Servlet + * @param errorPageURL The value of the errorpage attribute from the page + * directive or null + * @param needsSession The value of the session attribute from the + * page directive + * @param bufferSize The value of the buffer attribute from the page + * directive + * @param autoFlush The value of the autoflush attribute from the page + * directive + * @throws java.io.IOException during creation of JspWriter + * @throws IllegalStateException if out not correctly initialized + * @throws IllegalArgumentException If one of the given parameters + * is invalid + */ + @Override + public void initialize(Servlet servlet, ServletRequest request, ServletResponse response, String errorPageURL, boolean needsSession, int bufferSize, boolean autoFlush) throws IOException, IllegalStateException, IllegalArgumentException { + + } + + /** + *

+ * This method shall "reset" the internal state of a PageContext, releasing + * all internal references, and preparing the PageContext for potential + * reuse by a later invocation of initialize(). This method is typically + * called from JspFactory.releasePageContext(). + *

+ *

+ * Subclasses shall envelope this method. + *

+ *

+ * This method should not be used by page or tag library authors. + */ + @Override + public void release() { + + } + + /** + * The current value of the session object (an HttpSession). + * + * @return the HttpSession for this PageContext or null + */ + @Override + public HttpSession getSession() { + return request.getSession(); + } + + /** + * The current value of the page object (In a Servlet environment, + * this is an instance of javax.servlet.Servlet). + * + * @return the Page implementation class instance associated + * with this PageContext + */ + @Override + public Object getPage() { + return null; + } + + /** + * The current value of the request object (a ServletRequest). + * + * @return The ServletRequest for this PageContext + */ + @Override + public ServletRequest getRequest() { + return request; + } + + /** + * The current value of the response object (a ServletResponse). + * + * @return the ServletResponse for this PageContext + */ + @Override + public ServletResponse getResponse() { + return null; + } + + /** + * The current value of the exception object (an Exception). + * + * @return any exception passed to this as an errorpage + */ + @Override + public Exception getException() { + return null; + } + + /** + * The ServletConfig instance. + * + * @return the ServletConfig for this PageContext + */ + @Override + public ServletConfig getServletConfig() { + return null; + } + + /** + * The ServletContext instance. + * + * @return the ServletContext for this PageContext + */ + @Override + public ServletContext getServletContext() { + return null; + } + + /** + *

+ * This method is used to re-direct, or "forward" the current + * ServletRequest and ServletResponse to another active component in + * the application. + *

+ *

+ * If the relativeUrlPath begins with a "/" then the URL specified + * is calculated relative to the DOCROOT of the ServletContext + * for this JSP. If the path does not begin with a "/" then the URL + * specified is calculated relative to the URL of the request that was + * mapped to the calling JSP. + *

+ *

+ * It is only valid to call this method from a Thread + * executing within a _jspService(...) method of a JSP. + *

+ *

+ * Once this method has been called successfully, it is illegal for the + * calling Thread to attempt to modify the + * ServletResponse object. Any such attempt to do so, shall result + * in undefined behavior. Typically, callers immediately return from + * _jspService(...) after calling this method. + *

+ * + * @param relativeUrlPath specifies the relative URL path to the target + * resource as described above + * @throws IllegalStateException if ServletResponse is not + * in a state where a forward can be performed + * @throws javax.servlet.ServletException if the page that was forwarded to throws + * a ServletException + * @throws java.io.IOException if an I/O error occurred while forwarding + */ + @Override + public void forward(String relativeUrlPath) throws ServletException, IOException { + request.getRequestDispatcher(relativeUrlPath).forward(request, getResponse()); + } + + /** + *

+ * Causes the resource specified to be processed as part of the current + * ServletRequest and ServletResponse being processed by the calling Thread. + * The output of the target resources processing of the request is written + * directly to the ServletResponse output stream. + *

+ *

+ * The current JspWriter "out" for this JSP is flushed as a side-effect + * of this call, prior to processing the include. + *

+ *

+ * If the relativeUrlPath begins with a "/" then the URL specified + * is calculated relative to the DOCROOT of the ServletContext + * for this JSP. If the path does not begin with a "/" then the URL + * specified is calculated relative to the URL of the request that was + * mapped to the calling JSP. + *

+ *

+ * It is only valid to call this method from a Thread + * executing within a _jspService(...) method of a JSP. + *

+ * + * @param relativeUrlPath specifies the relative URL path to the target + * resource to be included + * @throws javax.servlet.ServletException if the page that was forwarded to throws + * a ServletException + * @throws java.io.IOException if an I/O error occurred while forwarding + */ + @Override + public void include(String relativeUrlPath) throws ServletException, IOException { + request.getRequestDispatcher(relativeUrlPath).include(request, getResponse()); + } + + /** + *

+ * Causes the resource specified to be processed as part of the current + * ServletRequest and ServletResponse being processed by the calling Thread. + * The output of the target resources processing of the request is written + * directly to the current JspWriter returned by a call to getOut(). + *

+ *

+ * If flush is true, The current JspWriter "out" for this JSP + * is flushed as a side-effect of this call, prior to processing + * the include. Otherwise, the JspWriter "out" is not flushed. + *

+ *

+ * If the relativeUrlPath begins with a "/" then the URL specified + * is calculated relative to the DOCROOT of the ServletContext + * for this JSP. If the path does not begin with a "/" then the URL + * specified is calculated relative to the URL of the request that was + * mapped to the calling JSP. + *

+ *

+ * It is only valid to call this method from a Thread + * executing within a _jspService(...) method of a JSP. + *

+ * + * @param relativeUrlPath specifies the relative URL path to the + * target resource to be included + * @param flush True if the JspWriter is to be flushed before the include, + * or false if not. + * @throws javax.servlet.ServletException if the page that was forwarded to throws + * a ServletException + * @throws java.io.IOException if an I/O error occurred while forwarding + * @since JSP 2.0 + */ + @Override + public void include(String relativeUrlPath, boolean flush) throws ServletException, IOException { + + request.getRequestDispatcher(relativeUrlPath).include(request, getResponse()); + } + + /** + *

+ * This method is intended to process an unhandled 'page' level + * exception by forwarding the exception to the specified + * error page for this JSP. If forwarding is not possible (for + * example because the response has already been committed), an + * implementation dependent mechanism should be used to invoke + * the error page (e.g. "including" the error page instead). + *

+ *

+ * If no error page is defined in the page, the exception should + * be rethrown so that the standard servlet error handling + * takes over. + *

+ *

+ * A JSP implementation class shall typically clean up any local state + * prior to invoking this and will return immediately thereafter. It is + * illegal to generate any output to the client, or to modify any + * ServletResponse state after invoking this call. + *

+ *

+ * This method is kept for backwards compatiblity reasons. Newly + * generated code should use PageContext.handlePageException(Throwable). + * + * @param e the exception to be handled + * @throws javax.servlet.ServletException if an error occurs while invoking the error page + * @throws java.io.IOException if an I/O error occurred while invoking the error + * page + * @throws NullPointerException if the exception is null + * @see #handlePageException(Throwable) + */ + @Override + public void handlePageException(Exception e) throws ServletException, IOException { + + } + + /** + *

+ * This method is intended to process an unhandled 'page' level + * exception by forwarding the exception to the specified + * error page for this JSP. If forwarding is not possible (for + * example because the response has already been committed), an + * implementation dependent mechanism should be used to invoke + * the error page (e.g. "including" the error page instead). + *

+ *

+ * If no error page is defined in the page, the exception should + * be rethrown so that the standard servlet error handling + * takes over. + *

+ *

+ * This method is intended to process an unhandled "page" level exception + * by redirecting the exception to either the specified error page for this + * JSP, or if none was specified, to perform some implementation dependent + * action. + *

+ *

+ * A JSP implementation class shall typically clean up any local state + * prior to invoking this and will return immediately thereafter. It is + * illegal to generate any output to the client, or to modify any + * ServletResponse state after invoking this call. + * + * @param t the throwable to be handled + * @throws javax.servlet.ServletException if an error occurs while invoking the error page + * @throws java.io.IOException if an I/O error occurred while invoking the error + * page + * @throws NullPointerException if the exception is null + * @see #handlePageException(Exception) + */ + @Override + public void handlePageException(Throwable t) throws ServletException, IOException { + + } } diff --git a/hex_utils/src/hex/utils/maps/CoercionMap.java b/hex_utils/src/hex/utils/coercion/CoercionMap.java similarity index 99% rename from hex_utils/src/hex/utils/maps/CoercionMap.java rename to hex_utils/src/hex/utils/coercion/CoercionMap.java index c337e9f..60dc1c4 100644 --- a/hex_utils/src/hex/utils/maps/CoercionMap.java +++ b/hex_utils/src/hex/utils/coercion/CoercionMap.java @@ -1,4 +1,4 @@ -package hex.utils.maps; +package hex.utils.coercion; import hex.utils.coercion.Coercible; import hex.utils.coercion.CoercionException; diff --git a/hex_utils/src/hex/utils/coercion/CoercionObject.java b/hex_utils/src/hex/utils/coercion/CoercionObject.java new file mode 100644 index 0000000..34cd938 --- /dev/null +++ b/hex_utils/src/hex/utils/coercion/CoercionObject.java @@ -0,0 +1,40 @@ +package hex.utils.coercion; + +import hex.utils.Inflection; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; + +/** + * Created by jason on 15-01-03. + */ +public class CoercionObject implements CoercionMap { + + private final Object source; + + public CoercionObject(Object source) { + this.source = source; + } + + @Override + public Object get(Object index) { + if(source instanceof Map) return ((Map)source).get(index); + String propertyName = (String)index; + try { + Method getter = source.getClass().getMethod(Inflection.toGetter(propertyName)); + return getter.invoke(source); + } catch (NoSuchMethodException e) { + return null; // don't have the property, return null like a proper map would + } catch (InvocationTargetException e) { + Throwable x = e.getCause(); + if(x instanceof RuntimeException) { + throw (RuntimeException)x; + } else { + throw new RuntimeException(String.format(Errors.PROPERTY_GETTER_THREW_EXCEPTION, propertyName), x); + } + } catch (IllegalAccessException e) { + throw new UnsupportedOperationException(String.format(Errors.PROPERTY_GETTER_INSUFFICIENT_ACCESS, propertyName), e); + } + } +} diff --git a/hex_utils/src/hex/utils/coercion/Errors.java b/hex_utils/src/hex/utils/coercion/Errors.java new file mode 100644 index 0000000..b5a79a1 --- /dev/null +++ b/hex_utils/src/hex/utils/coercion/Errors.java @@ -0,0 +1,12 @@ +package hex.utils.coercion; + +/** + * Created by jason on 15-01-03. + */ +public class Errors { + public static final String PROPERTY_GETTER_THREW_EXCEPTION = + "Getter for property [%s] threw exception."; + + public static final String PROPERTY_GETTER_INSUFFICIENT_ACCESS = + "Getter for property[%s] has less than public access."; +} diff --git a/hex_utils/src/hex/utils/collections/CoercionArray.java b/hex_utils/src/hex/utils/collections/CoercionArray.java index 4b0ecac..209080d 100644 --- a/hex_utils/src/hex/utils/collections/CoercionArray.java +++ b/hex_utils/src/hex/utils/collections/CoercionArray.java @@ -1,6 +1,6 @@ package hex.utils.collections; -import hex.utils.maps.CoercionMap; +import hex.utils.coercion.CoercionMap; import java.lang.reflect.Array; diff --git a/hex_utils/src/hex/utils/maps/PropertyMap.java b/hex_utils/src/hex/utils/maps/PropertyMap.java index a69e0a4..5e1222a 100644 --- a/hex_utils/src/hex/utils/maps/PropertyMap.java +++ b/hex_utils/src/hex/utils/maps/PropertyMap.java @@ -2,6 +2,7 @@ import hex.utils.coercion.Coercible; import hex.utils.coercion.CoercionException; +import hex.utils.coercion.CoercionMap; import hex.utils.generics.TypeVariables; import java.lang.reflect.Field; diff --git a/hex_utils/test/hex/utils/maps/CoercionMapTest.java b/hex_utils/test/hex/utils/maps/CoercionMapTest.java index 3930520..731c56b 100644 --- a/hex_utils/test/hex/utils/maps/CoercionMapTest.java +++ b/hex_utils/test/hex/utils/maps/CoercionMapTest.java @@ -3,6 +3,7 @@ import hex.utils.Memo; import hex.utils.coercion.Coercible; import hex.utils.coercion.CoercionException; +import hex.utils.coercion.CoercionMap; import hex.utils.test.Book; import hex.utils.test.matchers.IntArrayMatcher; import org.junit.Rule;