001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.http.api.responses; 007 008import org.apache.velocity.Template; 009import org.apache.velocity.VelocityContext; 010import org.apache.velocity.app.VelocityEngine; 011import org.apache.velocity.context.Context; 012import org.apache.velocity.tools.generic.EscapeTool; 013 014import org.fcrepo.config.FedoraPropsConfig; 015import org.fcrepo.http.api.FedoraSearch; 016import org.fcrepo.http.commons.responses.ViewHelpers; 017import org.fcrepo.search.api.Condition; 018import org.fcrepo.search.api.SearchResult; 019import org.slf4j.Logger; 020 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.OutputStream; 024import java.io.PrintWriter; 025import java.lang.annotation.Annotation; 026import java.lang.reflect.Type; 027import java.net.URL; 028import java.nio.charset.StandardCharsets; 029import java.util.Arrays; 030import java.util.Properties; 031 032import javax.annotation.PostConstruct; 033import javax.inject.Inject; 034import javax.servlet.http.HttpServletRequest; 035import javax.ws.rs.Produces; 036import javax.ws.rs.WebApplicationException; 037import javax.ws.rs.core.MediaType; 038import javax.ws.rs.core.MultivaluedMap; 039import javax.ws.rs.core.UriInfo; 040import javax.ws.rs.ext.MessageBodyWriter; 041import javax.ws.rs.ext.Provider; 042 043import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_HTML_WITH_CHARSET; 044import static org.slf4j.LoggerFactory.getLogger; 045 046/** 047 * HTML writer for search results 048 * 049 * @author awoods 050 * @since 2020-08-04 051 */ 052@Provider 053@Produces({TEXT_HTML_WITH_CHARSET}) 054public class SearchResultProvider implements MessageBodyWriter<SearchResult> { 055 056 @Inject 057 UriInfo uriInfo; 058 059 @Inject 060 HttpServletRequest request; 061 062 @Inject 063 private FedoraPropsConfig fedoraPropsConfig; 064 065 private static final EscapeTool escapeTool = new EscapeTool(); 066 067 private final VelocityEngine velocity = new VelocityEngine(); 068 069 // Location in the classpath where Velocity templates are to be found. 070 private static final String templatesLocation = "/views"; 071 072 private static final String templateFilenameExtension = ".vsl"; 073 074 private static final String velocityPropertiesLocation = "/velocity.properties"; 075 076 private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance(); 077 078 private static final Logger LOGGER = getLogger(SearchResultProvider.class); 079 080 @PostConstruct 081 void init() throws IOException { 082 LOGGER.trace("Velocity engine initializing..."); 083 final Properties properties = new Properties(); 084 final var velocityLog = fedoraPropsConfig.getVelocityLog().toString(); 085 LOGGER.debug("Setting Velocity runtime log: {}", velocityLog); 086 properties.setProperty("runtime.log", velocityLog); 087 088 final URL propertiesUrl = getClass().getResource(velocityPropertiesLocation); 089 090 LOGGER.debug("Using Velocity configuration from {}", propertiesUrl); 091 try (final InputStream propertiesStream = propertiesUrl.openStream()) { 092 properties.load(propertiesStream); 093 } 094 velocity.init(properties); 095 LOGGER.trace("Velocity engine initialized."); 096 } 097 098 @Override 099 public boolean isWriteable(final Class<?> type, 100 final Type genericType, 101 final Annotation[] annotations, 102 final MediaType mediaType) { 103 return SearchResult.class.isAssignableFrom(type); 104 } 105 106 @Override 107 public long getSize(final SearchResult result, 108 final Class<?> type, 109 final Type genericType, 110 final Annotation[] annotations, 111 final MediaType mediaType) { 112 return -1; 113 } 114 115 @Override 116 public void writeTo(final SearchResult result, 117 final Class<?> type, 118 final Type genericType, 119 final Annotation[] annotations, 120 final MediaType mediaType, 121 final MultivaluedMap<String, Object> httpHeaders, 122 final OutputStream entityStream) 123 throws WebApplicationException { 124 125 final Template template = velocity.getTemplate(getTemplateLocation("search")); 126 127 final Context context; 128 context = getContext(); 129 context.put("searchResults", result); 130 131 // The contract of MessageBodyWriter<T> is _not_ to close the stream after writing to it 132 final PrintWriter writer = new PrintWriter(entityStream, false, StandardCharsets.UTF_8); 133 template.merge(context, writer); 134 writer.flush(); 135 } 136 137 private Context getContext() { 138 139 final Context context = new VelocityContext(); 140 final String[] baseUrl = uriInfo.getBaseUri().getPath().split("/"); 141 if (baseUrl.length > 0) { 142 final String staticBaseUrl = String.join("/", Arrays.copyOf(baseUrl, baseUrl.length - 1)); 143 context.put("staticBaseUrl", staticBaseUrl); 144 } else { 145 context.put("staticBaseUrl", "/"); 146 } 147 final var searchPage = uriInfo.getBaseUriBuilder().clone().path(FedoraSearch.class).toString(); 148 context.put("searchPage", searchPage); 149 context.put("fields", 150 Arrays.stream(Condition.Field.values()).map(Condition.Field::toString).toArray(String[]::new)); 151 context.put("operators", Arrays.stream(Condition.Operator.values()).map(Condition.Operator::getStringValue) 152 .toArray(String[]::new)); 153 context.put("isOriginalResource", null); 154 context.put("helpers", VIEW_HELPERS); 155 context.put("esc", escapeTool); 156 context.put("uriInfo", uriInfo); 157 return context; 158 } 159 160 private static String getTemplateLocation(final String templateName) { 161 return templatesLocation + "/" + templateName.replace(':', '-') + templateFilenameExtension; 162 } 163 164}