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