package ameba.db.ebean.internal;
import ameba.db.dsl.QueryDSL;
import ameba.db.ebean.EbeanFeature;
import ameba.db.ebean.EbeanUtils;
import ameba.db.ebean.filter.EbeanExprInvoker;
import ameba.db.ebean.filter.WhereExprApplier;
import ameba.db.model.Finder;
import ameba.message.filtering.EntityFieldsFilteringFeature;
import ameba.message.internal.BeanPathProperties;
import io.ebean.*;
import io.ebean.bean.BeanCollection;
import io.ebean.common.BeanList;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.querydefn.OrmQueryDetail;
import io.ebeaninternal.server.querydefn.OrmQueryProperties;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.hk2.api.ServiceLocator;
import javax.annotation.PostConstruct;
import javax.annotation.Priority;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
/**
* <p>ModelInterceptor class.</p>
*
* @author icode
* @since 0.1.6e
*
*/
@Singleton
@Priority(Priorities.ENTITY_CODER)
public class ModelInterceptor implements WriterInterceptor {
private final static Integer SYS_DEFAULT_PER_PAGE = 20;
private static String FIELDS_PARAM_NAME = "fields";
private static String SORT_PARAM_NAME = "sort";
private static String PAGE_PARAM_NAME = "page";
private static String PER_PAGE_PARAM_NAME = "per_page";
private static String REQ_TOTAL_COUNT_PARAM_NAME = "req_count";
private static String REQ_TOTAL_COUNT_HEADER_NAME = "X-Total-Count";
private static String FILTER_PARAM_NAME = "filter";
private static Integer DEFAULT_PER_PAGE = SYS_DEFAULT_PER_PAGE;
private static int MAX_PER_PAGE = 1000;
@Context
private Provider<Configuration> configurationProvider;
@Context
private Provider<UriInfo> uriInfoProvider;
@Inject
private ServiceLocator locator;
/**
* <p>getFieldsParamName.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getFieldsParamName() {
return FIELDS_PARAM_NAME;
}
/**
* <p>getSortParamName.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getSortParamName() {
return SORT_PARAM_NAME;
}
/**
* <p>getPageParamName.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getPageParamName() {
return PAGE_PARAM_NAME;
}
/**
* <p>getPerPageParamName.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getPerPageParamName() {
return PER_PAGE_PARAM_NAME;
}
/**
* <p>getReqTotalCountParamName.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getReqTotalCountParamName() {
return REQ_TOTAL_COUNT_PARAM_NAME;
}
/**
* <p>getReqTotalCountHeaderName.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getReqTotalCountHeaderName() {
return REQ_TOTAL_COUNT_HEADER_NAME;
}
/**
* <p>getFilterParamName.</p>
*
* @return a {@link java.lang.String} object.
*/
public static String getFilterParamName() {
return FILTER_PARAM_NAME;
}
/**
* <p>getDefaultPerPage.</p>
*
* @return a {@link java.lang.Integer} object.
*/
public static Integer getDefaultPerPage() {
return DEFAULT_PER_PAGE;
}
/**
* Return a single Integer parameter.
*
* @param list a {@link java.util.List} object.
* @return a {@link java.lang.Integer} object.
*/
protected static Integer getSingleIntegerParam(List<String> list) {
String s = getSingleParam(list);
if (s != null) {
try {
return Integer.valueOf(s);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* Return a single parameter value.
*
* @param list a {@link java.util.List} object.
* @return a {@link java.lang.String} object.
*/
protected static String getSingleParam(List<String> list) {
if (list != null && list.size() == 1) {
return list.get(0);
}
return null;
}
/**
* apply query parameters to select/fetch
* <br>
* ?fields=id,name,filed1(p1,p2,p3)
* <br>
* ?fields=(id,name,filed1(p1,p2,p3))
*
* @param query query
* @param queryParams a {@link javax.ws.rs.core.MultivaluedMap} object.
*/
public static void applyFetchProperties(MultivaluedMap<String, String> queryParams, Query query) {
List<String> selectables = queryParams.get(FIELDS_PARAM_NAME);
if (selectables != null) {
StringBuilder selectBuilder = new StringBuilder();
OrmQueryDetail detail = ((SpiQuery) query).getDetail();
OrmQueryProperties base = detail.getChunk(null, false);
if (base != null && StringUtils.isNotBlank(base.getProperties())) {
// 获取已经设置的select
selectBuilder.append(base.getProperties());
}
for (String s : selectables) {
if (StringUtils.isBlank(s)) {
continue;
}
if (!s.startsWith("(")) {
s = "(" + s;
}
if (!s.startsWith(")")) {
s += ")";
}
BeanPathProperties pathProperties = BeanPathProperties.parse(s);
for (BeanPathProperties.Props props : pathProperties.getPathProps()) {
String path = props.getPath();
String propsStr = props.getPropertiesAsString();
if (StringUtils.isEmpty(path)) {
if (selectBuilder.length() > 0) {
selectBuilder.append(",");
}
if (propsStr.length() > 0)
selectBuilder.append(propsStr);
} else if (StringUtils.isNotBlank(path)) {
FetchConfig config = null;
// 获取已经存在的fetch
OrmQueryProperties fetch = detail.getChunk(path, false);
if (fetch != null && StringUtils.isNotBlank(fetch.getProperties())) {
// 增加客户端传入值
propsStr = fetch.getProperties() + "," + propsStr;
config = fetch.getFetchConfig();
}
query.fetch(path, propsStr, config);
}
}
}
if (selectBuilder.length() > 0) {
query.select(selectBuilder.toString());
}
}
}
/**
* <p>applyOrderBy.</p>
*
* @param queryParams a {@link javax.ws.rs.core.MultivaluedMap} object.
* @param query a {@link io.ebean.Query} object.
*/
public static void applyOrderBy(MultivaluedMap<String, String> queryParams, Query query) {
List<String> orders = queryParams.get(SORT_PARAM_NAME);
if (orders != null && orders.size() > 0) {
OrderBy orderBy = query.orderBy();
for (String order : orders) {
EbeanUtils.appendOrder(orderBy, order);
}
}
}
/**
* <p>fetchRowCount.</p>
*
* @param queryParams a {@link javax.ws.rs.core.MultivaluedMap} object.
* @param query a {@link io.ebean.Query} object.
* @return a {@link io.ebean.FutureRowCount} object.
*/
public static FutureRowCount fetchRowCount(MultivaluedMap<String, String> queryParams, Query query) {
String reqTotalCount = getSingleParam(queryParams.get(REQ_TOTAL_COUNT_PARAM_NAME));
if (reqTotalCount != null && !"false".equalsIgnoreCase(reqTotalCount) && !"0".equals(reqTotalCount)) {
return query.findFutureCount();
}
return null;
}
/**
* <p>applyPageList.</p>
*
* @param queryParams a {@link javax.ws.rs.core.MultivaluedMap} object.
* @param query a {@link io.ebean.Query} object.
* @return a {@link io.ebean.FutureRowCount} object.
*/
public static FutureRowCount applyPageList(MultivaluedMap<String, String> queryParams, Query query) {
FutureRowCount futureRowCount = fetchRowCount(queryParams, query);
applyPageConfig(queryParams, query);
return futureRowCount;
}
/**
* <p>applyPageConfig.</p>
*
* @param queryParams a {@link javax.ws.rs.core.MultivaluedMap} object.
* @param query a {@link io.ebean.Query} object.
*/
public static void applyPageConfig(MultivaluedMap<String, String> queryParams, Query query) {
Integer maxRows = getSingleIntegerParam(queryParams.get(PER_PAGE_PARAM_NAME));
if (maxRows == null && DEFAULT_PER_PAGE != null && DEFAULT_PER_PAGE > 0) {
maxRows = DEFAULT_PER_PAGE;
}
if (maxRows != null) {
if (maxRows <= 0) {
maxRows = SYS_DEFAULT_PER_PAGE;
}
if (MAX_PER_PAGE != -1 && maxRows > MAX_PER_PAGE) {
maxRows = MAX_PER_PAGE;
}
query.setMaxRows(maxRows);
}
Integer firstRow = getSingleIntegerParam(queryParams.get(PAGE_PARAM_NAME));
if (firstRow != null && maxRows != null) {
if (firstRow < 1) {
firstRow = 1;
}
firstRow--;
firstRow = firstRow * maxRows;
query.setFirstRow(firstRow);
}
}
/**
* /path?filter=p.in(1,2)c.eq('ddd')d.startWith('a')or(f.eq('a')g.startWith(2))
*
* @param queryParams uri query params
* @param query query
* @param locator a {@link org.glassfish.hk2.api.ServiceLocator} object.
* @param <T> a T object.
*/
public static <T> void applyFilter(MultivaluedMap<String, String> queryParams,
SpiQuery<T> query,
ServiceLocator locator) {
List<String> wheres = queryParams.get(FILTER_PARAM_NAME);
if (wheres != null && wheres.size() > 0) {
EbeanExprInvoker invoker = new EbeanExprInvoker(query, locator);
WhereExprApplier<T> applier = WhereExprApplier.create(query.where());
for (String w : wheres) {
QueryDSL.invoke(
w,
invoker,
applier
);
}
}
}
/**
* apply uri query parameter on query
*
* @param queryParams uri query params
* @param query Query
* @param needPageList need page list
* @return page list count or null
* @see #applyFetchProperties
* @see #applyFilter
* @see #applyOrderBy
* @see #applyPageList
* @param locator a {@link org.glassfish.hk2.api.ServiceLocator} object.
*/
@SuppressWarnings("unchecked")
public static FutureRowCount applyUriQuery(MultivaluedMap<String, String> queryParams,
SpiQuery query,
ServiceLocator locator, boolean needPageList) {
Set<String> inv = query.validate();
applyFetchProperties(queryParams, query);
applyFilter(queryParams, query, locator);
applyOrderBy(queryParams, query);
EbeanUtils.checkQuery(
(SpiQuery<?>) query,
inv,
null,
locator
);
if (needPageList)
return applyPageList(queryParams, query);
return null;
}
/**
* <p>applyUriQuery.</p>
*
* @param queryParams a {@link javax.ws.rs.core.MultivaluedMap} object.
* @param query a {@link io.ebean.Query} object.
* @return a {@link io.ebean.FutureRowCount} object.
* @param locator a {@link org.glassfish.hk2.api.ServiceLocator} object.
*/
public static FutureRowCount applyUriQuery(MultivaluedMap<String, String> queryParams,
SpiQuery query,
ServiceLocator locator) {
return applyUriQuery(queryParams, query, locator, true);
}
/**
* <p>applyRowCountHeader.</p>
*
* @param headerParams a {@link javax.ws.rs.core.MultivaluedMap} object.
* @param query a {@link io.ebean.Query} object.
* @param rowCount a {@link io.ebean.FutureRowCount} object.
*/
public static void applyRowCountHeader(MultivaluedMap<String, Object> headerParams, Query query, FutureRowCount rowCount) {
if (rowCount != null) {
try {
headerParams.putSingle(REQ_TOTAL_COUNT_HEADER_NAME, rowCount.get());
} catch (InterruptedException | ExecutionException e) {
headerParams.putSingle(REQ_TOTAL_COUNT_HEADER_NAME, query.findCount());
}
}
}
@PostConstruct
private void init() {
Configuration configuration = configurationProvider.get();
final String fieldsParamName = (String) configuration.getProperty(EntityFieldsFilteringFeature.QUERY_FIELDS_PARAM_NAME);
FIELDS_PARAM_NAME = StringUtils.isNotBlank(fieldsParamName) ? fieldsParamName : FIELDS_PARAM_NAME;
final String sortParamName = (String) configuration.getProperty(EbeanFeature.SORT_PARAM_NAME);
SORT_PARAM_NAME = StringUtils.isNotBlank(sortParamName) ? sortParamName : SORT_PARAM_NAME;
final String pageParamName = (String) configuration.getProperty(EbeanFeature.PAGE_PARAM_NAME);
PAGE_PARAM_NAME = StringUtils.isNotBlank(pageParamName) ? pageParamName : PAGE_PARAM_NAME;
final String perPageParamName = (String) configuration.getProperty(EbeanFeature.PER_PAGE_PARAM_NAME);
PER_PAGE_PARAM_NAME = StringUtils.isNotBlank(perPageParamName) ? perPageParamName : PER_PAGE_PARAM_NAME;
final String reqTotalCountParamName = (String) configuration.getProperty(EbeanFeature.REQ_TOTAL_COUNT_PARAM_NAME);
REQ_TOTAL_COUNT_PARAM_NAME = StringUtils.isNotBlank(reqTotalCountParamName) ? perPageParamName : REQ_TOTAL_COUNT_PARAM_NAME;
final String reqTotalCountHeaderName = (String) configuration.getProperty(EbeanFeature.REQ_TOTAL_COUNT_HEADER_NAME);
REQ_TOTAL_COUNT_HEADER_NAME = StringUtils.isNotBlank(reqTotalCountHeaderName) ? perPageParamName : REQ_TOTAL_COUNT_HEADER_NAME;
final String filterParamName = (String) configuration.getProperty(EbeanFeature.FILTER_PARAM_NAME);
FILTER_PARAM_NAME = StringUtils.isNotBlank(filterParamName) ? filterParamName : FILTER_PARAM_NAME;
final String defaultPerPage = (String) configuration.getProperty(EbeanFeature.DEFAULT_PER_PAGE_PARAM_NAME);
if (StringUtils.isNotBlank(defaultPerPage)) {
try {
DEFAULT_PER_PAGE = Integer.parseInt(defaultPerPage);
} catch (Exception e) {
DEFAULT_PER_PAGE = null;
}
}
final String maxPerPage = (String) configuration.getProperty(EbeanFeature.MAX_PER_PAGE_PARAM_NAME);
if (StringUtils.isNotBlank(maxPerPage)) {
try {
MAX_PER_PAGE = Integer.parseInt(maxPerPage);
} catch (Exception e) {
MAX_PER_PAGE = -1;
}
}
}
/**
* <p>isWritable.</p>
*
* @param type a {@link java.lang.Class} object.
* @return a boolean.
*/
public boolean isWritable(Class<?> type) {
return Finder.class.isAssignableFrom(type)
|| Query.class.isAssignableFrom(type)
|| ExpressionList.class.isAssignableFrom(type)
|| FutureList.class.isAssignableFrom(type);
}
/** {@inheritDoc} */
@Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
Object o = context.getEntity();
if (o != null && isWritable(o.getClass())) {
MultivaluedMap<String, String> queryParams = uriInfoProvider.get().getQueryParameters();
SpiQuery query = null;
if (o instanceof Finder) {
query = (SpiQuery) ((Finder) o).query();
} else if (o instanceof Query) {
query = (SpiQuery) o;
} else if (o instanceof ExpressionList) {
query = (SpiQuery) ((ExpressionList) o).query();
}
if (query != null) {
FutureRowCount rowCount = applyUriQuery(queryParams, query, locator);
BeanList list;
if (o instanceof FutureList) {
list = (BeanList) ((FutureList) o).getUnchecked();
} else {
list = (BeanList) query.findList();
}
applyRowCountHeader(context.getHeaders(), query, rowCount);
List result = list.getActualList();
context.setEntity(result);
Class clazz = result.getClass();
context.setType(clazz);
context.setGenericType(query.getBeanType());
}
} else if (o instanceof BeanCollection && !BeanCollection.class.isAssignableFrom(context.getType())) {
context.setEntity(o.getClass());
}
context.proceed();
}
}