package ameba.mvc.template.internal; import ameba.Ameba; import ameba.core.Application; import com.google.common.base.CaseFormat; import com.google.common.collect.Lists; import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.Predicate; import org.apache.commons.lang3.ArrayUtils; import org.glassfish.jersey.internal.util.PropertiesHelper; import org.glassfish.jersey.message.MessageBodyWorkers; import org.glassfish.jersey.message.internal.MediaTypes; import org.glassfish.jersey.server.ExtendedUriInfo; import org.glassfish.jersey.uri.UriTemplate; import javax.inject.Inject; import javax.inject.Provider; import javax.ws.rs.ConstrainedTo; import javax.ws.rs.Produces; import javax.ws.rs.RuntimeType; import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyWriter; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Type; import java.util.Collection; import java.util.List; import java.util.Map; /** * default template body writer * <br><br> * sort template find * <br> * 1. resource method name * <br> * 2. _protected/ + req path LOWER_UNDERSCORE * <br> * 3. _protected/ + req raw path * <br> * 4. req path LOWER_UNDERSCORE * <br> * 5. req raw path * <br> * 6. index * <br> * 7. default view * * @author icode */ @ConstrainedTo(RuntimeType.SERVER) @Produces({"text/html", "application/xhtml+xml", "application/x-ms-application"}) final class DataViewMessageBodyWriter implements MessageBodyWriter<Object> { private static final String DISABLE_DATA_VIEW = "data.view.disabled"; private static final String DISABLE_DEFAULT_DATA_VIEW = "data.view.default.disabled"; private static final MediaType LOW_IE_DEFAULT_REQ_TYPE = new MediaType("application", "x-ms-application"); private static final List<MediaType> TEMPLATE_PRODUCES = Lists.newArrayList( MediaType.TEXT_HTML_TYPE, MediaType.APPLICATION_XHTML_XML_TYPE, LOW_IE_DEFAULT_REQ_TYPE, MediaType.WILDCARD_TYPE ); private static final String DATA_VIEW_DEFAULT_KEY_PRE = "data.view.default."; private static final String DATA_VIEW_LIST_KEY = DATA_VIEW_DEFAULT_KEY_PRE + "list"; private static final String DATA_VIEW_ITEM_KEY = DATA_VIEW_DEFAULT_KEY_PRE + "item"; private static final String DATA_VIEW_NULL_KEY = DATA_VIEW_DEFAULT_KEY_PRE + "empty"; private static final String DEFAULT_DATA_VIEW_PAGE_DIR = Viewables.PROTECTED_DIR_PATH + "/default/"; private static final String DEFAULT_DATA_LIST = DEFAULT_DATA_VIEW_PAGE_DIR + "list"; private static final String DEFAULT_DATA_ITEM = DEFAULT_DATA_VIEW_PAGE_DIR + "item"; private static final String DEFAULT_DATA_NULL = DEFAULT_DATA_VIEW_PAGE_DIR + "empty"; private final boolean dataViewDisabled; private final boolean defaultDataViewDisabled; private final String dataViewList; private final String dataViewItem; private final String dataViewNull; @Context private Provider<ContainerRequestContext> requestProvider; @Context private Provider<ResourceInfo> resourceInfoProvider; @Context private Provider<ExtendedUriInfo> uriInfoProvider; @Context private Provider<MessageBodyWorkers> workersProvider; /** * <p>Constructor for DataViewMessageBodyWriter.</p> * * @param application a {@link ameba.core.Application} object. */ @Inject public DataViewMessageBodyWriter(Application application) { dataViewDisabled = "true".equals(application.getProperty(DISABLE_DATA_VIEW)); defaultDataViewDisabled = "true".equals(application.getProperty(DISABLE_DEFAULT_DATA_VIEW)); Map<String, Object> properties = application.getProperties(); dataViewList = PropertiesHelper.getValue(properties, DATA_VIEW_LIST_KEY, DEFAULT_DATA_LIST, null); dataViewItem = PropertiesHelper.getValue(properties, DATA_VIEW_ITEM_KEY, DEFAULT_DATA_ITEM, null); dataViewNull = PropertiesHelper.getValue(properties, DATA_VIEW_NULL_KEY, DEFAULT_DATA_NULL, null); } /** * {@inheritDoc} */ @Override public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) { String[] p; return !dataViewDisabled && -1 != ListUtils.indexOf(requestProvider.get().getAcceptableMediaTypes(), this::isSupportMediaType) && ((p = TemplateHelper.getProduces(annotations)) == null || -1 != ArrayUtils.indexOf(p, (Predicate<String>) stringType -> { if (stringType.equals(MediaType.WILDCARD)) return true; MediaType mediaType1 = MediaType.valueOf(stringType); return isSupportMediaType(mediaType1); })); } /** {@inheritDoc} */ @Override public long getSize(final Object viewable, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) { return -1; } /** {@inheritDoc} */ @Override public void writeTo(final Object entity, final Class<?> type, final Type genericType, final Annotation[] annotations, MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException, WebApplicationException { if (mediaType == null || MediaTypes.isWildcard(mediaType) || (mediaType.getType().equals(LOW_IE_DEFAULT_REQ_TYPE.getType()) && mediaType.getSubtype().equals(LOW_IE_DEFAULT_REQ_TYPE.getSubtype()))) { mediaType = MediaType.TEXT_HTML_TYPE; } List<String> templates = Lists.newArrayList(); ResourceInfo resourceInfo = resourceInfoProvider.get(); // 1. resource method name // 2. _protected/ + req path LOWER_UNDERSCORE // 3. _protected/ + req raw path // 4. req path LOWER_UNDERSCORE // 5. req raw path // 6. index // 7. default view if (resourceInfo != null && resourceInfo.getResourceMethod() != null) { templates.add(resourceInfo.getResourceMethod().getName()); } // xxx/{a_b}.httl == xxx/{aB}.httl String path = getTemplatePath(uriInfoProvider.get()); String _path = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, path); if (!_path.equals(path)) { templates.add(Viewables.PROTECTED_DIR_PATH + _path); } templates.add(Viewables.PROTECTED_DIR_PATH + path); if (!_path.equals(path)) { templates.add(_path); } templates.add(path); templates.add("index"); if (!defaultDataViewDisabled) { if (entity == null || (entity instanceof Collection && ((Collection) entity).size() == 0) || (entity.getClass().isArray() && Array.getLength(entity) == 0)) { templates.add(dataViewNull); } else if (isItem(entity)) { templates.add(dataViewItem); } else { templates.add(dataViewList); } } Class clazz = null; if (resourceInfo != null) { clazz = resourceInfo.getResourceClass(); } if (clazz == null) { List<Object> res = uriInfoProvider.get().getMatchedResources(); if (res != null && res.size() > 0) { clazz = res.get(0).getClass(); } } if (clazz == null) { clazz = Ameba.class; } workersProvider.get().getMessageBodyWriter( ImplicitViewable.class, ImplicitViewable.class, annotations, mediaType) .writeTo(new ImplicitViewable(templates, entity, clazz), ImplicitViewable.class, ImplicitViewable.class, annotations, mediaType, httpHeaders, entityStream); } private boolean isItem(Object entity) { return !(entity instanceof Collection) && !entity.getClass().isArray(); } private String getTemplatePath(ExtendedUriInfo uriInfo) { StringBuilder builder = new StringBuilder(); for (UriTemplate template : uriInfo.getMatchedTemplates()) { List<String> variables = template.getTemplateVariables(); String[] args = new String[variables.size()]; for (int i = 0; i < args.length; i++) { args[i] = "{" + variables.get(i) + "}"; } String uri = template.createURI(args); if (!uri.equals("/") && !uri.equals("")) builder.insert(0, uri); } return builder.toString(); } private boolean isSupportMediaType(MediaType mediaType) { for (MediaType type : TEMPLATE_PRODUCES) { if (mediaType.getType().equalsIgnoreCase(type.getType()) && mediaType.getSubtype().equalsIgnoreCase(type.getSubtype())) { return true; } } return false; } }