act.util.PropertySpec Maven / Gradle / Ivy
package act.util;
/*-
* #%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.app.ActionContext;
import act.cli.CliContext;
import act.cli.CliSession;
import act.controller.meta.HandlerMethodMetaInfo;
import org.osgl.$;
import org.osgl.Lang;
import org.osgl.util.C;
import org.osgl.util.S;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Mark on a method (could be cli command, or controller action)
* to specify the fields to be exported.
* This annotation is only effective when there is one and only one
* type of object returned, as a single instance or a collection of instances, e.g
*
* {@literal @}PropertySpec({"firstName","lastName","email"})
* public List<Employee> getEmployees(String search) {
* List<Employee> retList = EmployeeDao.find(search).asList();
* return retList;
* }
*
* Suppose the request accept {@code application/json} type, then only the following
* field of the {@code Employee} instances will be exported in JSON output:
*
* - firstName
* - lastName
* - email
*
*
* When the result is to be presented on a {@link CliSession} and
* {@code PropertySpec} annotation is presented, either {@link act.cli.TableView}
* or {@link act.util.JsonView} can be used to define the presenting style.
* If both {@code TableView} and {@code JsonView} are found on the method
* then {@code JsonView} is the winner. If non of them is presented then
* {@code JsonView} will be used by default
*
*
* When the result is to be write to an {@link org.osgl.http.H.Response}, and
* {@code PropertySpec} annotation is presented on the controller action method,
* then the return value (if not of type {@link org.osgl.mvc.result.Result}) will
* be serialized into a JSON string and the filter will effect and impact the
* JSON string
*
* @see act.cli.TableView
* @see act.util.JsonView
* @see FastJsonPropertyPreFilter
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PropertySpec {
/**
* Specify the object fields to be displayed in final result. E.g.
*
* {@literal @}PropertySpec({"firstName","lastName","email"})
*
* You can specify multiple fields in one string, with fields
* separated with one of the following character: {@code ,;|}
*
* {@literal @}PropertySpec("firstName,lastName,email")
*
* You can use {@code as} to specify the label, e.g.
*
* {@literal @}PropertySpec("fn as First name,ln as Last name,email as Email")
*
* If there are multiple levels of objects, use {@code .} or {@code /} to
* express the traverse path:
*
* {@literal @}PropertySpec("fn,ln,contact.address.street,contact.address.city,contact.email")
*
* Instead of specifying fields to be exported, it can also specify the fields to be
* excluded from output with the symbol {@code -}, e.g.
*
* {@literal @}PropertySpec("-password,-salary")
*
* when symbol {@code -} is used to specify the excluded fields, then all the fields
* without symbol {@code -} in the list will be ignored. However it can still use
* {@code as} notation to specify the label. E.g.
*
* {@literal @}PropertySpec("-password,-salary,fn as firstName,ln as lastName")
*
* @return the field specification
*/
String[] value() default {};
/**
* Specify the spec for command line interface output
*
* If not specified, then it will use the spec specified in {@link #value()}
* when output to CLI
*
* @return the field specification for CLI
* @see #value()
*/
String[] cli() default {};
/**
* Specify the spec for http response output
*
* If not specified, then it will use the spec specified in {@link #value()}
* when output to http response
*
* @return the field specification for http
* @see #value()
*/
String[] http() default {};
/**
* Capture the {@code PropertySpec} annotation meta info in bytecode scanning phase
*/
class MetaInfo {
// split "fn as firstName" into "fn" and "firstName"
private static Pattern p = Pattern.compile("\\s+as\\s+", Pattern.CASE_INSENSITIVE);
public static class Spec extends $.T3, Set, Map> {
Spec() {
super(C.newList(), C.newSet(), C.newMap());
}
List outputs() {
return _1;
}
Set excluded() {
return _2;
}
Map labels() {
return _3;
}
boolean isEmpty() {
return _1.isEmpty() && _2.isEmpty() && _3.isEmpty();
}
public Lang._MappingStage applyTo(Lang._MappingStage stage) {
if (!outputs().isEmpty()) {
stage.filter(S.join(",", outputs()));
} else if (!excluded().isEmpty()) {
stage.filter(S.join(",", C.list(excluded()).map(S.F.prepend("-"))));
}
if (!labels().isEmpty()) {
stage.withHeadMapping(labels());
}
return stage;
}
}
private static Spec newSpec() {
return new Spec();
}
private Spec common = newSpec();
private Spec cli = newSpec();
private Spec http = newSpec();
private String commonRaw;
private String cliRaw;
private String httpRaw;
public void onValue(String value) {
commonRaw = value;
_on(value, common);
}
public void onCli(String value) {
cliRaw = value;
_on(value, cli);
}
public void onHttp(String value) {
httpRaw = value;
_on(value, http);
}
public void ensureValid() {
if (common.isEmpty() && http.isEmpty() && cli.isEmpty()) {
throw new IllegalStateException("no spec defined");
}
}
private void _on(String string, Spec spec) {
String[] sa = string.split("[,;]+");
for (String s: sa) {
s = s.trim();
if (s.startsWith("-")) {
spec.excluded().add(s.substring(1));
spec.outputs().clear();
} else {
String[] sa0 = p.split(s);
if (sa0.length > 1) {
String k = sa0[0].trim(), v = sa0[1].trim();
spec.labels().put(k, v);
if (spec.excluded().isEmpty()) {
spec.outputs().add(k);
}
} else if (spec.excluded().isEmpty()) {
spec.outputs().add(s.trim());
}
}
}
}
@Deprecated
public List outputFields() {
return C.list(common.outputs());
}
public String raw(ActContext context) {
if (null == context) {
return commonRaw;
} else if (context instanceof ActionContext) {
return S.blank(httpRaw) ? commonRaw : httpRaw;
} else if (context instanceof CliContext) {
return S.blank(cliRaw) ? commonRaw : cliRaw;
} else {
// mail context is unlikely to happen
throw new IllegalStateException("context not applied: " + context);
}
}
public List outputFields(ActContext context) {
Spec spec = spec(context);
return null == spec ? C.list() : spec.outputs();
}
public List outputFieldsAndLabel(ActContext context) {
Spec spec = spec(context);
if (null == spec) {
return C.list();
}
List outputs = spec.outputs();
List pairs = new ArrayList<>();
for (String f : outputs) {
S.Pair pair = S.pair(f, spec.labels().get(f));
pairs.add(pair);
}
return pairs;
}
public List outputFieldsForHttp() {
Spec spec = httpSpec();
return null == spec ? C.list() : spec.outputs();
}
public List labels(List outputs, ActContext context) {
List retList = new ArrayList<>();
for (String f : outputs) {
retList.add(label(f, context));
}
return retList;
}
public List labels2(List outputs, ActContext context) {
List retList = new ArrayList<>();
for (S.Pair pair : outputs) {
retList.add(label(pair, context));
}
return retList;
}
public Map labelMapping() {
return C.Map(common.labels());
}
public Map labelMapping(ActContext context) {
return null == context ? labelMapping() : C.Map(spec(context).labels());
}
public Set excludedFields(ActContext context) {
Spec spec = spec(context);
return null == spec ? C.Set() : C.Set(spec.excluded());
}
public Set excludeFieldsForHttp() {
Spec spec = httpSpec();
return null == spec ? C.Set() : C.Set(spec.excluded());
}
public String label(String field, ActContext context) {
String lbl = spec(context).labels().get(field);
return null == lbl ? field : lbl;
}
public String label(S.Pair field, ActContext context) {
String lbl = spec(context).labels().get(field._1);
String defLabel = field._2;
if (null == defLabel) {
defLabel = field._1;
}
return null == lbl ? defLabel : lbl;
}
public void setLabelIfNotFound(String field, String label, ActContext context) {
Map map = spec(context).labels();
if (!map.containsKey(field)) {
map.put(field, label);
}
}
public Lang._MappingStage applyTo(Lang._MappingStage mappingStage, ActContext context) {
return spec(context).applyTo(mappingStage);
}
private Spec httpSpec() {
return null == http || http.isEmpty() ? common : http;
}
private Spec spec(ActContext context) {
if (null == context) {
return common;
}
if (context instanceof ActionContext) {
return null == http || http.isEmpty() ? common : http;
} else if (context instanceof CliContext) {
return null == cli || cli.isEmpty() ? common : cli;
} else {
// mail context is unlikely to happen
throw new IllegalStateException("context not applied: " + context);
}
}
public static MetaInfo withCurrentNoConsume(MetaInfo builtIn, ActContext context) {
return _withCurrent(builtIn, context, false);
}
public static MetaInfo withCurrent(MetaInfo builtIn, ActContext context) {
return _withCurrent(builtIn, context, true);
}
private static MetaInfo _withCurrent(MetaInfo builtIn, ActContext context, boolean consume) {
// see https://github.com/actframework/actframework/issues/1118
if (consume && null != context) {
if (context.isPropertySpecConsumed()) {
return null;
}
context.markPropertySpecConsumed();
}
MetaInfo retVal = builtIn;
MetaInfo spec = currentSpec.get();
if (null != spec) {
retVal = spec;
} else {
String s = PropertySpec.current.get();
if (S.notBlank(s)) {
spec = new PropertySpec.MetaInfo();
if (context instanceof CliContext) {
spec.onCli(s);
} else {
spec.onHttp(s);
}
retVal = spec;
}
}
if (context instanceof ActionContext) {
ActionContext actionContext = (ActionContext) context;
actionContext.propertySpec(retVal);
}
return retVal;
}
public static MetaInfo withCurrent(HandlerMethodMetaInfo methodMetaInfo, ActContext context) {
MetaInfo builtIn = null == methodMetaInfo ? null : methodMetaInfo.propertySpec();
return withCurrent(builtIn, context);
}
}
ThreadLocal current = new ThreadLocal<>();
ThreadLocal currentSpec = new ThreadLocal<>();
}