package act.controller; /*- * #%L * ACT Framework * %% * Copyright (C) 2014 - 2017 ActFramework * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import act.Act; import act.app.ActionContext; import act.conf.AppConfigKey; import act.controller.meta.HandlerMethodMetaInfo; import act.data.Versioned; import act.route.Router; import act.util.DisableFastJsonCircularReferenceDetect; import act.util.FastJsonIterable; import act.util.PropertySpec; import act.view.*; import org.osgl.$; import org.osgl.Osgl; import org.osgl.http.H; import org.osgl.mvc.result.*; import org.osgl.storage.ISObject; import org.osgl.util.C; import org.osgl.util.E; import org.osgl.util.IO; import org.osgl.util.S; import javax.inject.Inject; import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Collection; import java.util.Map; import static org.osgl.http.H.Format.*; /** * Mark a class as Controller, which contains at least one of the following: * <ul> * <li>Action handler method</li> * <li>Any one of Before/After/Exception/Finally interceptor</li> * </ul> */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface Controller { /** * Indicate the context path for all action methods declared * in this controller. * <p/> * <p>Default value: "{@code /}"</p> * * @return the controller context path */ String value() default "/"; /** * Specify the port(s) this controller's action method shall be * routed from. * * @return the port name * @see AppConfigKey#NAMED_PORTS */ String[] port() default {}; /** * Provides utilities for controller action methods to emit rendering results */ class Util { public static final Ok OK = Ok.get(); public static final Created CREATED = Created.INSTANCE; public static final Result CREATED_JSON = new Result(H.Status.CREATED, "{\"message\": \"Created\"}") {}; public static final Result CREATED_XML = new Result(H.Status.CREATED, "<?xml version=\"1.0\" ?><message>Created</message>") {}; public static final Result OK_JSON = new Result(H.Status.OK, "{\"message\": \"Okay\"}") {}; public static final Result OK_XML = new Result(H.Status.OK, "<?xml version=\"1.0\" ?><message>Okay</message>") {}; public static final NoContent NO_CONTENT = NoContent.get(); /** * Returns an {@link Ok} result */ public static Result ok() { H.Format accept = ActionContext.current().accept(); if (H.Format.JSON == accept) { return OK_JSON; } else if (H.Format.XML == accept) { return OK_XML; } return OK; } /** * Returns a {@link Created} result * * @param resourceGetUrl the URL to access the new resource been created * @return the result as described */ public static Created created(String resourceGetUrl) { return Created.withLocation(resourceGetUrl); } /** * Return a {@link Created} result * @return the result as described */ public static Created created() { return Created.INSTANCE; } public static NotModified notModified() { return NotModified.get(); } public static NotModified notModified(String etag, Object... args) { return NotModified.of(etag, args); } /** * Returns a {@link Accepted} result * * @param statusMonitorUrl the URL to check the request process status * @return the result as described */ public static Result accepted(String statusMonitorUrl) { return new Accepted(statusMonitorUrl); } public static Result notAcceptable() { return NotAcceptable.get(); } public static Result notAcceptable(String msg, Object... args) { return NotAcceptable.of(msg, args); } /** * Returns an {@link NotFound} result */ public static Result notFound() { return ActNotFound.create(); } /** * Returns an {@link NotFound} result with custom message * template and arguments. The final message is rendered with * the template and arguments using {@link String#format(String, Object...)} * * @param msg the message template * @param args the message argument */ public static Result notFound(String msg, Object... args) { return ActNotFound.create(msg, args); } /** * Throws out an {@link NotFound} result if the object specified is * {@code null} * * @param o the object to be evaluated */ public static <T> T notFoundIfNull(T o) { if (null == o) { throw ActNotFound.create(); } return o; } /** * Throws out an {@link NotFound} result with custom message template and * arguments if the object specified is {@code null}. The final message is * rendered with the template and arguments using * {@link String#format(String, Object...)} * * @param o the object to be evaluated * @param msg the message template * @param args the message argument */ public static <T> T notFoundIfNull(T o, String msg, Object... args) { if (null == o) { throw ActNotFound.create(msg, args); } return o; } /** * Throws out an {@link NotFound} result if the boolean expression specified * is {@code true} * {@code null} * * @param test the boolean expression to be evaluated */ public static void notFoundIf(boolean test) { if (test) { throw ActNotFound.create(); } } /** * Throws out an {@link NotFound} result with custom message template and * arguments if the expression specified is {@code true}. The final message is * rendered with the template and arguments using * {@link String#format(String, Object...)} * * @param test the boolean expression * @param msg the message template * @param args the message argument */ public static void notFoundIf(boolean test, String msg, Object... args) { if (test) { throw ActNotFound.create(msg, args); } } /** * Throws out an {@link NotFound} result if the boolean expression specified * is {@code false} * {@code null} * * @param test the boolean expression to be evaluated */ public static void notFoundIfNot(boolean test) { notFoundIf(!test); } /** * Throws out an {@link NotFound} result with custom message template and * arguments if the expression specified is {@code false}. The final message is * rendered with the template and arguments using * {@link String#format(String, Object...)} * * @param test the boolean expression * @param msg the message template * @param args the message argument */ public static void notFoundIfNot(boolean test, String msg, Object... args) { notFoundIf(!test, msg, args); } public static BadRequest badRequest() { return ActBadRequest.create(); } public static BadRequest badRequest(String msg, Object... args) { return ActBadRequest.create(msg, args); } public static void badRequestIf(boolean test) { if (test) { throw ActBadRequest.create(); } } public static void badRequestIf(boolean test, String msg, Object... args) { if (test) { throw ActBadRequest.create(msg, args); } } public static void badRequestIfBlank(String test) { if (S.blank(test)) { throw ActBadRequest.create(); } } public static void badRequestIfBlank(String test, String msg, Object... args) { if (S.blank(test)) { throw ActBadRequest.create(msg, args); } } public static void badRequestIfNull(Object test) { if (null == test) { throw ActBadRequest.create(); } } public static void badRequestIfNull(Object test, String msg, Object... args) { if (null == test) { throw ActBadRequest.create(msg, args); } } public static void badRequestIfNot(boolean test) { if (!test) { throw ActBadRequest.create(); } } public static void badRequestIfNot(boolean test, String msg, Object... args) { badRequestIf(!test, msg, args); } public static Conflict conflict() { return ActConflict.create(); } public static Conflict conflict(String message, Object... args) { return ActConflict.create(message, args); } public static void conflictIf(boolean test) { if (test) { throw ActConflict.create(); } } public static void conflictIf(boolean test, String message, Object... args) { if (test) { throw ActConflict.create(message, args); } } public static void conflictIfNot(boolean test) { conflictIf(!test); } public static void conflictIfNot(boolean test, String message, Object... args) { conflictIf(!test, message, args); } public static Unauthorized unauthorized() { return ActUnauthorized.create(); } public static Unauthorized unauthorized(String realm) { return ActUnauthorized.create(realm); } public static void unauthorizedIf(boolean test) { if (test) { throw ActUnauthorized.create(); } } public static void unauthorizedIf(boolean test, String realm) { if (test) { throw ActUnauthorized.create(realm); } } public static void unauthorizedIfNot(boolean test) { unauthorizedIf(!test); } public static void unauthorizedIfNot(boolean test, String realm) { unauthorizedIf(!test, realm); } /** * Returns a {@link Forbidden} result */ public static Forbidden forbidden() { return ActForbidden.create(); } /** * Returns a {@link Forbidden} result with custom message * template and arguments. The final message is rendered with * the template and arguments using {@link String#format(String, Object...)} * * @param msg the message template * @param args the message argument */ public static Forbidden forbidden(String msg, Object... args) { return ActForbidden.create(msg, args); } /** * Throws a {@link Forbidden} result if the test condition is {@code true} * * @param test the test condition */ public static void forbiddenIf(boolean test) { if (test) { throw ActForbidden.create(); } } /** * Throws a {@link Forbidden} result if the test condition is {@code false} * * @param test the test condition */ public static void forbiddenIfNot(boolean test) { forbiddenIf(!test); } /** * Throws a {@link Forbidden} result if test condition is {@code true} * * @param test the test condition * @param msg the message format template * @param args the message format arguments */ public static void forbiddenIf(boolean test, String msg, Object... args) { if (test) { throw ActForbidden.create(msg, args); } } /** * Throws a {@link Forbidden} result if the test condition is {@code false} * * @param test the test condition * @param msg the message format template * @param args the message format arguments */ public static void forbiddenIfNot(boolean test, String msg, Object... args) { forbiddenIf(!test, msg, args); } public static Redirect redirect(String url, Object... args) { url = S.fmt(url, args); if (url.contains(".") || url.contains("(")) { String inferFullActionPath = Router.inferFullActionPath(url); if (inferFullActionPath != url) { url = ActionContext.current().router().reverseRoute(url); } } else { if (!url.startsWith("/")) { ActionContext context = ActionContext.current(); String urlContext = context.urlContext(); if (S.notBlank(urlContext)) { url = S.pathConcat(urlContext, '/', url); } } } return Redirect.of(url); } public static Redirect redirect(String url, Map reverseRoutingArguments) { url = Router.inferFullActionPath(url); url = ActionContext.current().router().reverseRoute(url, reverseRoutingArguments); return Redirect.of(url); } public static void redirectIf(boolean test, String url, Object... args) { if (test) { throw redirect(url, args); } } public static void redirectIfNot(boolean test, String url, Object... args) { redirectIf(!test, url, args); } public static void redirectIf(boolean test, String url, Map reverseRoutingArguments) { if (test) { throw redirect(url, reverseRoutingArguments); } } public static void redirectIfNot(boolean test, String url, Map reverseRoutingArguments) { redirectIf(!test, url, reverseRoutingArguments); } /** * Returns a {@link RenderText} result with specified message template * and args. The final message is rendered with the template and arguments using * {@link String#format(String, Object...)} * * @param msg the message format template * @param args the message format arguments */ public static RenderText text(String msg, Object... args) { return RenderText.of(successStatus(), msg, args); } /** * Alias of {@link #text(String, Object...)} * @param msg the message format template * @param args the message format arguments * @return the result */ public static RenderText renderText(String msg, Object... args) { return text(msg, args); } /** * Returns a {@link RenderText} result with specified message template * and args. The final message is rendered with the template and arguments using * {@link String#format(String, Object...)} * * @param msg the message format template * @param args the message format arguments * @return the result */ public static RenderHtml html(String msg, Object... args) { return RenderHtml.of(successStatus(), msg, args); } /** * Alias of {@link #html(String, Object...)} * @param msg the message format template * @param args the message format arguments * @return the result */ public static RenderHtml renderHtml(String msg, Object args) { return html(msg, args); } /** * Returns a {@link RenderJSON} result with specified message template * and args. The final message is rendered with the template and arguments using * {@link String#format(String, Object...)} * * @param msg the message format template * @param args the message format arguments * @return the result */ public static RenderJSON json(String msg, Object... args) { return RenderJSON.of(successStatus(), msg, args); } /** * Alias of {@link #json(String, Object...)} * @param msg the message format template * @param args the message format arguments * @return the result */ public static RenderJSON renderJson(String msg, Object... args) { return json(msg, args); } /** * Returns a {@link RenderJSON} result with any object. This method will * call underline JSON serializer to transform the object into a JSON string * * @param data the data to be rendered as JSON string * @return the result */ public static RenderJSON json(Object data) { return RenderJSON.of(successStatus(), data); } /** * Alias of {@link #json(Object)} * @param data the data to be rendered as JSON string * @return the result */ public static RenderJSON renderJson(Object data) { return json(data); } /** * Returns a {@link RenderJsonMap} result with any object. This method will * generate a JSON object out from the {@link ActionContext#renderArgs}. * The response is always in JSON format and ignores the HTTP `Accept` * header setting * @param data the varargs of Object to be put into the JSON map * @return the result */ public static RenderJsonMap jsonMap(Object... data) { return RenderJsonMap.get(); } /** * Alias of {@link #jsonMap(Object...)} * @param data the data to be put into the JSON map * @return the result */ public static RenderJsonMap renderJsonMap(Object ... data) { return jsonMap(data); } /** * Returns a {@link RenderXML} result with specified message template * and args. The final message is rendered with the template and arguments using * {@link String#format(String, Object...)} * * @param msg the message format template * @param args the message format arguments * @return the result */ public static RenderXML xml(String msg, Object... args) { return RenderXML.of(successStatus(), msg, args); } /** * Alias of {@link #xml(String, Object...)} * @param msg the message format template * @param args the message format arguments * @return the result */ public static RenderXML renderXml(String msg, Object... args) { return xml(msg, args); } /** * Returns a {@link RenderBinary} result with an {@link ISObject} instance. The result will render * the binary using "inline" content disposition * * @param sobj the {@link ISObject} instance * @return the result */ public static RenderBinary binary(ISObject sobj) { return new RenderBinary(sobj.asInputStream(), sobj.getAttribute(ISObject.ATTR_FILE_NAME), sobj.getAttribute(ISObject.ATTR_CONTENT_TYPE), true); } /** * Alias of {@link #binary(ISObject)} * @param sobj the {@link ISObject} instance * @return the result */ public static RenderBinary renderBinary(ISObject sobj) { return binary(sobj); } /** * Returns a {@link RenderBinary} result with an {@link ISObject} instance. The result will render * the binary using "attachment" content disposition * * @param sobj the {@link ISObject} instance */ public static RenderBinary download(ISObject sobj) { return new RenderBinary(sobj.asInputStream(), sobj.getAttribute(ISObject.ATTR_FILE_NAME), sobj.getAttribute(ISObject.ATTR_CONTENT_TYPE), false); } /** * Returns a {@link RenderBinary} result with a file. The result will render * the binary using "inline" content disposition. * * @param file the file to be rendered * @return a result */ public static RenderBinary binary(File file) { return new RenderBinary(file); } /** * Alias of {@link #binary(File)} * @param file the file to be rendered * @return a result */ public static RenderBinary renderBinary(File file) { return binary(file); } /** * Returns a {@link RenderBinary} result with a delayed output stream writer. * The result will render the binary using "inline" content disposition. * * @param outputStreamWriter the delayed writer * @return the result */ public static RenderBinary binary($.Function<OutputStream, ?> outputStreamWriter) { return new RenderBinary(outputStreamWriter); } /** * Alias of {@link #binary(Osgl.Function)} * @param outputStreamWriter the delayed writer * @return the result */ public static RenderBinary renderBinary($.Function<OutputStream, ?> outputStreamWriter) { return binary(outputStreamWriter); } /** * Returns a {@link RenderBinary} result with a file. The result will render * the binary using "attachment" content disposition. * * @param file the file to be rendered */ public static RenderBinary download(File file) { return new RenderBinary(file, file.getName(), false); } /** * Render barcode for given content * @param content the content to generate the barcode * @return the barcode as a binary result */ public static ZXingResult barcode(String content) { return ZXingResult.barcode(content); } /** * Alias of {@link #barcode(String)} * @param content the content to generate the barcode * @return the barcode as a binary result */ public static ZXingResult renderBarcode(String content) { return barcode(content); } /** * Render QRCode for given content * @param content the content to generate the qrcode * @return the qrcode as a binary result */ public static ZXingResult qrcode(String content) { return ZXingResult.qrcode(content); } /** * Alias of {@link #qrcode(String)} * @param content the content to generate the barcode * @return the barcode as a binary result */ public static ZXingResult renderQrcode(String content) { return qrcode(content); } /** * Returns a {@link RenderTemplate} result with a render arguments map. * Note the template path should be set via {@link ActionContext#templatePath(String)} * method * * @param args the template arguments * @return a result to render template */ public static RenderTemplate template(Map<String, Object> args) { return RenderTemplate.of(args); } /** * Alias of {@link #template(Map)} * * @param args the template arguments * @return a result to render template */ public static RenderTemplate renderTemplate(Map<String, Object> args) { return template(args); } /** * This method is deprecated, please use {@link #template(Object...)} instead * * @param args template argument list */ public static RenderTemplate renderTemplate(Object... args) { return RenderTemplate.get(); } /** * Kind of like {@link #render(Object...)}, the only differences is this method force to render a template * without regarding to the request format * * @param args template argument list */ public static RenderTemplate template(Object... args) { return RenderTemplate.get(ActionContext.current().successStatus()); } /** * The caller to this magic {@code render} method is subject to byte code enhancement. All * parameter passed into this method will be put into the application context via * {@link ActionContext#renderArg(String, Object)} using the variable name found in the * local variable table. If the first argument is of type String and there is no variable name * associated with that variable then it will be treated as template path and get set to the * context via {@link ActionContext#templatePath(String)} method. * <p>This method returns different render results depends on the request format</p> * <table> * <tr> * <th>Format</th> * <th>Result type</th> * </tr> * <tr> * <td>{@link org.osgl.http.H.Format#json}</td> * <td>A JSON string that map the arguments to their own local variable names</td> * </tr> * <tr> * <td>{@link org.osgl.http.H.Format#html} or any other text formats</td> * <td>{@link RenderTemplate}</td> * </tr> * <tr> * <td>{@link org.osgl.http.H.Format#pdf} or any other binary format</td> * <td>If first argument is of type File or InputStream, then outbound the * content as a binary stream, otherwise throw out {@link org.osgl.exception.UnsupportedException}</td> * </tr> * </table> * * @param args any argument that can be put into the returned JSON/XML data or as template arguments */ public static RenderAny render(Object... args) { return RenderAny.get(); } public static Result inferResult(Result r, ActionContext actionContext) { return r; } public static Result inferPrimitiveResult(Object v, ActionContext actionContext, boolean requireJSON, boolean requireXML, boolean isArray) { if (requireJSON) { return RenderJSON.of(C.map("result", v)); } else if (requireXML) { return RenderXML.of(S.concat("<result>", S.string(v), "</result>")); } else { H.Format fmt = actionContext.accept(); final H.Status status = actionContext.successStatus(); if (HTML == fmt || H.Format.UNKNOWN == fmt) { String s = isArray ? $.toString2(v) : v.toString(); return RenderHtml.of(status, s); } if (TXT == fmt || CSV == fmt) { return RenderText.of(status, fmt, status.toString()); } throw E.unexpected("Cannot apply text result to format: %s", fmt); } } public static Result inferResult(Map<String, Object> map, ActionContext actionContext) { if (actionContext.acceptJson()) { return RenderJSON.of(map); } return RenderTemplate.of(map); } /** * @param array * @param actionContext * @return */ public static Result inferResult(Object[] array, ActionContext actionContext) { if (actionContext.acceptJson()) { return RenderJSON.of(actionContext.successStatus(), array); } throw E.tbd("render template with render args in array"); } /** * Infer {@link Result} from an {@link InputStream}. If the current context is in * {@code JSON} format then it will render a {@link RenderJSON JSON} result from the content of the * input stream. Otherwise, it will render a {@link RenderBinary binary} result from the inputstream * * @param is the inputstream * @param actionContext * @return a Result inferred from the inputstream specified */ public static Result inferResult(InputStream is, ActionContext actionContext) { if (actionContext.acceptJson()) { return RenderJSON.of(IO.readContentAsString(is)); } else { return new RenderBinary(is, null, true); } } /** * Infer {@link Result} from an {@link File}. If the current context is in * {@code JSON} format then it will render a {@link RenderJSON JSON} result from the content of the * file. Otherwise, it will render a {@link RenderBinary binary} result from the file specified * * @param file the file * @param actionContext * @return a Result inferred from the file specified */ public static Result inferResult(File file, ActionContext actionContext) { if (actionContext.acceptJson()) { return RenderJSON.of(IO.readContentAsString(file)); } else { return new RenderBinary(file); } } public static Result inferResult(ISObject sobj, ActionContext context) { if (context.acceptJson()) { return RenderJSON.of(sobj.asString()); } else { return binary(sobj); } } /** * Infer a {@link Result} from a {@link Object object} value v: * <ul> * <li>If v is {@code null} then null returned</li> * <li>If v is instance of {@code Result} then it is returned directly</li> * to infer the {@code Result}</li> * <li>If v is instance of {@code InputStream} then {@link #inferResult(InputStream, ActionContext)} is used * to infer the {@code Result}</li> * <li>If v is instance of {@code File} then {@link #inferResult(File, ActionContext)} is used * to infer the {@code Result}</li> * <li>If v is instance of {@code Map} then {@link #inferResult(Map, ActionContext)} is used * to infer the {@code Result}</li> * <li>If v is an array of {@code Object} then {@link #inferResult(Object[], ActionContext)} is used * to infer the {@code Result}</li> * </ul> * * @param meta the HandlerMethodMetaInfo * @param v the value to be rendered * @param context the action context * @param hasTemplate a boolean flag indicate if the current handler method has corresponding template * @return the rendered result */ public static Result inferResult(HandlerMethodMetaInfo meta, Object v, ActionContext context, boolean hasTemplate) { if (v instanceof Result) { return (Result)v; } final H.Request req = context.req(); final H.Status status = req.method() == H.Method.POST ? H.Status.CREATED : H.Status.OK; if (Act.isProd() && v instanceof Versioned && req.method().safe()) { processEtag(meta, v, context, req); } if (hasTemplate) { if (v instanceof Map) { return inferToTemplate(((Map) v), context); } return inferToTemplate(v, context); } boolean requireJSON = context.acceptJson(); boolean requireXML = !requireJSON && context.acceptXML(); if (null == v) { // the following code breaks before handler without returning result //return requireJSON ? RenderJSON.of("{}") : requireXML ? RenderXML.of("<result></result>") : null; return null; } else if ($.isSimpleType(v.getClass())) { boolean isArray = meta.returnType().getDescriptor().startsWith("["); return inferPrimitiveResult(v, context, requireJSON, requireXML, isArray); } else if (v instanceof InputStream) { return inferResult((InputStream) v, context); } else if (v instanceof File) { return inferResult((File) v, context); } else if (v instanceof ISObject) { return inferResult((ISObject) v, context); } else if (v instanceof Map) { return RenderJSON.of(v); } else { if (requireJSON) { // patch https://github.com/alibaba/fastjson/issues/478 if (meta.disableJsonCircularRefDetect()) { DisableFastJsonCircularReferenceDetect.option.set(true); } if (v instanceof Iterable && !(v instanceof Collection)) { v = new FastJsonIterable((Iterable) v); } PropertySpec.MetaInfo propertySpec = PropertySpec.MetaInfo.withCurrent(meta, context); try { if (null == propertySpec) { return RenderJSON.of(status, v); } return FilteredRenderJSON.get(status, v, propertySpec, context); } finally { if (meta.disableJsonCircularRefDetect()) { DisableFastJsonCircularReferenceDetect.option.set(false); } } } else if (context.acceptXML()) { PropertySpec.MetaInfo propertySpec = PropertySpec.MetaInfo.withCurrent(meta, context); return new FilteredRenderXML(v, propertySpec, context); } else if (context.accept() == H.Format.CSV) { PropertySpec.MetaInfo propertySpec = PropertySpec.MetaInfo.withCurrent(meta, context); return RenderCSV.get(status, v, propertySpec, context); } else { boolean isArray = meta.returnType().getDescriptor().startsWith("["); return inferPrimitiveResult(v, context, false, requireXML, isArray); } } } private static void processEtag(HandlerMethodMetaInfo meta, Object v, ActionContext context, H.Request req) { if (!(v instanceof Versioned)) { return; } String version = ((Versioned) v)._version(); String etagVersion = etag(meta, version); if (req.etagMatches(etagVersion)) { throw NotModified.get(); } else { context.resp().etag(etagVersion); } } private static String etag(HandlerMethodMetaInfo meta, String version) { return S.newBuffer(version).append(meta.hashCode()).toString(); } private static Result inferToTemplate(Object v, ActionContext actionContext) { actionContext.renderArg("result", v); return RenderTemplate.get(); } private static Result inferToTemplate(Map map, ActionContext actionContext) { return RenderTemplate.of(map); } private static H.Status successStatus() { return ActionContext.current().successStatus(); } } /** * Controller class extends this class automatically get `ActionContext` injected * as a field */ class Base extends Util { @Inject protected ActionContext context; } }