/* * The MIT License * * Copyright 2013 Tim Boudreau. * * 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 com.mastfrog.acteur; import com.google.common.net.MediaType; import static com.mastfrog.acteur.Help.HELP_URL_PATTERN_SETTINGS_KEY; import com.mastfrog.acteur.headers.Headers; import com.mastfrog.acteur.headers.Method; import com.mastfrog.acteur.preconditions.Description; import com.mastfrog.acteur.util.CacheControlTypes; import com.mastfrog.acteur.util.Connection; import com.mastfrog.settings.Settings; import java.lang.reflect.Array; import java.nio.charset.Charset; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import javax.inject.Inject; import org.joda.time.DateTime; /** * * @author Tim Boudreau */ @Description("Provides a list of API calls this API supports") final class HelpPage extends Page { @Inject HelpPage(ActeurFactory af, Settings settings, DateTime serverStartTime) { String pattern = settings.getString(HELP_URL_PATTERN_SETTINGS_KEY, "^help$"); getResponseHeaders().addCacheControl(CacheControlTypes.Public); getResponseHeaders().setLastModified(serverStartTime); add(af.matchPath(pattern)); add(af.matchMethods(Method.GET)); add(af.sendNotModifiedIfIfModifiedSinceHeaderMatches()); add(HelpActeur.class); } private static class HelpActeur extends Acteur { private final boolean html; @Inject HelpActeur(Application app, HttpEvent evt, Charset charset) { this.html = "true".equals(evt.getParameter("html")); if (html) { add(Headers.CONTENT_TYPE, MediaType.HTML_UTF_8.withCharset(charset)); } setChunked(true); setResponseWriter(new HelpWriter(html, app)); add(Headers.CONNECTION, Connection.close); } @Override public void describeYourself(Map<String, Object> into) { into.put("Collects help info", true); } @Override public BaseState getState() { return new RespondWith(200); } public static final class HelpWriter extends ResponseWriter { private final boolean html; private final Application app; @Inject HelpWriter(boolean html, Application app) { this.html = html; this.app = app; } @Override public Status write(Event<?> evt, Output out, int iteration) throws Exception { Map<String, Object> help = app.describeYourself(); if (html) { StringBuilder sb = new StringBuilder("<html><head><style>body { font-family: 'Helvetica';}</style><title>") .append(app.getName()) .append(" API Help</title></head>\n<body>\n<h1>").append(app.getName()).append(" API Help</h1>\n"); Description des = app.getClass().getAnnotation(Description.class); if (des != null) { sb.append("<p>").append(des.value()).append("</p>\n"); } sb.append("<p><i style='font-size: 0.85em;'>Note that " + "URL matching expressions are relative to the " + "application base path, which can be set by passing " + "<code>--basepath $PATH</code> on the command-line " + "or set in a properties file." + "<i></p>"); writeOut(null, help, sb, null); sb.append("</body></html>\n"); out.write(sb.toString()); return Status.DONE; } else { out.writeObject(help); return Status.DONE; } } private String deBicapitalize(String s) { if (s == null) { return null; } StringBuilder sb = new StringBuilder(); if (s.endsWith("Page")) { s = s.substring(0, s.length() - "Page".length()); } if (s.endsWith("Resource")) { s = s.substring(0, s.length() - "Resource".length()); } boolean lastWasCaps = true; for (char c : s.toCharArray()) { if (Character.isUpperCase(c)) { if (!lastWasCaps) { sb.append(' '); } lastWasCaps = true; } else { lastWasCaps = false; } sb.append(c); } return sb.toString(); } @SuppressWarnings("unchecked") private StringBuilder writeOut(String key, Object object, StringBuilder sb, String parentKey) { boolean code = ("PathRegex".equals(parentKey) || "Path".equals(parentKey) || "Methods".equals(parentKey)) && "value".equals(key); String codeOpen = code ? "<code>" : ""; String codeClose = code ? "</code>" : ""; String humanized = deBicapitalize(key); if (key == null || object instanceof Map<?,?>) { Map<String, Object> m = Collections.checkedMap((Map<String,Object>) object, String.class, Object.class); if (key != null) { sb.append("\n<tr><th valign=\"left\" bgcolor='#FFEECC'>").append(humanized).append("</th><td>"); } sb.append("<table>\n"); List<String> sortedKeys = new LinkedList<>(m.keySet()); Collections.sort(sortedKeys); for (String k : sortedKeys) { Object val = m.get(k); sb.append(codeOpen);; writeOut(k, val, sb, key); sb.append(codeClose); if (key == null) { sb.append("\n<tr><td colspan=2><hr/></td></tr>\n"); } } sb.append("\n</table>\n"); if (key != null) { sb.append("\n</td></tr>\n"); } } else if (object instanceof CharSequence || object instanceof Boolean || object instanceof Number || object instanceof Enum) { sb.append("\n<tr><th bgcolor='#DDDDDD'>").append(humanized).append("</th><td>") .append(codeOpen) .append(object) .append(codeClose) .append("</td></tr>\n"); } else if (object != null && object.getClass().isArray()) { String s = toString(object);; sb.append("\n<tr><th bgcolor='#DDDDDD'>").append(humanized).append("</th><td>") .append(codeOpen) .append(s) .append(codeClose) .append("</td></tr>\n"); } else { sb.append("\n<tr><th bgcolor='#DDDDDD'>").append(humanized).append("</th><td>") .append(codeOpen) .append(object) .append(codeClose) .append("</td></tr>\n"); } return sb; } private String toString(Object o) { if (o.getClass().isArray()) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < Array.getLength(o); i++) { if (sb.length() > 0) { sb.append(", "); } sb.append(toString(Array.get(o, i))); } return sb.toString(); } else { return Objects.toString(o); } } } } }