001/** 002 * Copyright 2015 DuraSpace, Inc. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.fcrepo.http.api.responses; 017 018import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML; 019import static javax.ws.rs.core.MediaType.APPLICATION_XHTML_XML_TYPE; 020import static javax.ws.rs.core.MediaType.TEXT_HTML; 021import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE; 022import static com.google.common.collect.ImmutableMap.builder; 023import static com.hp.hpl.jena.graph.Node.ANY; 024import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty; 025import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource; 026import static org.apache.commons.lang3.StringUtils.isBlank; 027import static org.fcrepo.kernel.api.RdfLexicon.JCR_NAMESPACE; 028import static org.fcrepo.kernel.modeshape.rdf.JcrRdfTools.getRDFNamespaceForJcrNamespace; 029import static org.slf4j.LoggerFactory.getLogger; 030 031import java.io.IOException; 032import java.io.InputStream; 033import java.io.OutputStream; 034import java.io.OutputStreamWriter; 035import java.io.Writer; 036import java.lang.annotation.Annotation; 037import java.lang.reflect.Type; 038import java.net.URL; 039import java.util.ArrayList; 040import java.util.Arrays; 041import java.util.List; 042import java.util.Map; 043import java.util.Optional; 044import java.util.Properties; 045 046import javax.annotation.PostConstruct; 047import javax.jcr.RepositoryException; 048import javax.jcr.Session; 049import javax.jcr.nodetype.NodeType; 050import javax.jcr.nodetype.NodeTypeIterator; 051import javax.ws.rs.Produces; 052import javax.ws.rs.WebApplicationException; 053import javax.ws.rs.core.MediaType; 054import javax.ws.rs.core.MultivaluedMap; 055import javax.ws.rs.core.UriInfo; 056import javax.ws.rs.ext.MessageBodyWriter; 057import javax.ws.rs.ext.Provider; 058 059import com.google.common.collect.ImmutableList; 060import com.google.common.collect.ImmutableMap; 061import com.hp.hpl.jena.graph.Node; 062import com.hp.hpl.jena.rdf.model.Model; 063import org.apache.velocity.Template; 064import org.apache.velocity.VelocityContext; 065import org.apache.velocity.app.VelocityEngine; 066import org.apache.velocity.context.Context; 067import org.apache.velocity.tools.generic.EscapeTool; 068import org.apache.velocity.tools.generic.FieldTool; 069import org.fcrepo.http.commons.responses.HtmlTemplate; 070import org.fcrepo.http.commons.responses.ViewHelpers; 071import org.fcrepo.http.commons.session.SessionFactory; 072import org.fcrepo.kernel.api.RdfLexicon; 073import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 074import org.fcrepo.kernel.api.utils.iterators.RdfStream; 075import org.fcrepo.kernel.modeshape.rdf.impl.NamespaceRdfContext; 076import org.slf4j.Logger; 077import org.springframework.beans.factory.annotation.Autowired; 078 079/** 080 * Simple HTML provider for RdfStreams 081 * 082 * @author ajs6f 083 * @since Nov 19, 2013 084 */ 085@Provider 086@Produces({TEXT_HTML, APPLICATION_XHTML_XML}) 087public class StreamingBaseHtmlProvider implements MessageBodyWriter<RdfStream> { 088 089 090 @Autowired 091 SessionFactory sessionFactory; 092 093 @javax.ws.rs.core.Context 094 UriInfo uriInfo; 095 096 private static EscapeTool escapeTool = new EscapeTool(); 097 098 protected VelocityEngine velocity = new VelocityEngine(); 099 100 /** 101 * Location in the classpath where Velocity templates are to be found. 102 */ 103 public static final String templatesLocation = "/views"; 104 105 /** 106 * Location in the classpath where the common css file is to be found. 107 */ 108 public static final String commonCssLocation = "/views/common.css"; 109 110 /** 111 * Location in the classpath where the common javascript file is to be found. 112 */ 113 public static final String commonJsLocation = "/views/common.js"; 114 115 /** 116 * A map from String names for primary node types to the Velocity templates 117 * that should be used for those node types. 118 */ 119 protected Map<String, Template> templatesMap; 120 121 public static final String templateFilenameExtension = ".vsl"; 122 123 public static final String velocityPropertiesLocation = 124 "/velocity.properties"; 125 126 private static final ViewHelpers VIEW_HELPERS = ViewHelpers.getInstance(); 127 128 private static final Logger LOGGER = 129 getLogger(StreamingBaseHtmlProvider.class); 130 131 @PostConstruct 132 void init() throws IOException { 133 134 LOGGER.trace("Velocity engine initializing..."); 135 final Properties properties = new Properties(); 136 final URL propertiesUrl = 137 getClass().getResource(velocityPropertiesLocation); 138 LOGGER.debug("Using Velocity configuration from {}", propertiesUrl); 139 try (final InputStream propertiesStream = propertiesUrl.openStream()) { 140 properties.load(propertiesStream); 141 } 142 velocity.init(properties); 143 LOGGER.trace("Velocity engine initialized."); 144 145 LOGGER.trace("Assembling a map of node primary types -> templates..."); 146 final ImmutableMap.Builder<String, Template> templatesMapBuilder = builder(); 147 final Session session = sessionFactory.getInternalSession(); 148 try { 149 // we search all of the possible node primary types and mixins 150 for (final NodeTypeIterator primaryNodeTypes = 151 session.getWorkspace().getNodeTypeManager() 152 .getPrimaryNodeTypes(); primaryNodeTypes.hasNext();) { 153 final NodeType primaryNodeType = 154 primaryNodeTypes.nextNodeType(); 155 final String primaryNodeTypeName = 156 primaryNodeType.getName(); 157 158 // Create a list of the primary type and all its parents 159 final List<NodeType> nodeTypesList = new ArrayList<>(); 160 nodeTypesList.add(primaryNodeType); 161 nodeTypesList.addAll(Arrays.asList(primaryNodeType.getSupertypes())); 162 163 // Find a template that matches the primary type or one of its parents 164 nodeTypesList.stream() 165 .map(NodeType::getName) 166 .filter(x -> !isBlank(x) && velocity.resourceExists(getTemplateLocation(x))) 167 .findFirst() 168 .ifPresent(x -> addTemplate(primaryNodeTypeName, x, templatesMapBuilder)); 169 } 170 171 final List<String> otherTemplates = 172 ImmutableList.of("jcr:nodetypes", "node", "fcr:versions", "fcr:fixity"); 173 174 for (final String key : otherTemplates) { 175 final Template template = 176 velocity.getTemplate(getTemplateLocation(key)); 177 templatesMapBuilder.put(key, template); 178 } 179 180 templatesMap = templatesMapBuilder.build(); 181 182 } catch (final RepositoryException e) { 183 throw new RepositoryRuntimeException(e); 184 } 185 LOGGER.trace("Assembled template map."); 186 LOGGER.trace("HtmlProvider initialization complete."); 187 } 188 189 @Override 190 public void writeTo(final RdfStream rdfStream, final Class<?> type, 191 final Type genericType, final Annotation[] annotations, 192 final MediaType mediaType, 193 final MultivaluedMap<String, Object> httpHeaders, 194 final OutputStream entityStream) throws IOException { 195 196 try { 197 final RdfStream nsRdfStream = new NamespaceRdfContext(rdfStream.session()); 198 199 rdfStream.namespaces(nsRdfStream.namespaces()); 200 201 final Node subject = VIEW_HELPERS.getContentNode(rdfStream.topic()); 202 203 final Model model = rdfStream.asModel(); 204 205 final Template nodeTypeTemplate = getTemplate(model, subject, Arrays.asList(annotations)); 206 207 final Context context = getContext(model, subject); 208 209 // the contract of MessageBodyWriter<T> is _not_ to close the stream 210 // after writing to it 211 final Writer outWriter = new OutputStreamWriter(entityStream); 212 nodeTypeTemplate.merge(context, outWriter); 213 outWriter.flush(); 214 215 } catch (final RepositoryException e) { 216 throw new WebApplicationException(e); 217 } 218 219 } 220 221 protected Context getContext(final Model model, final Node subject) { 222 final FieldTool fieldTool = new FieldTool(); 223 224 final Context context = new VelocityContext(); 225 context.put("rdfLexicon", fieldTool.in(RdfLexicon.class)); 226 context.put("helpers", VIEW_HELPERS); 227 context.put("esc", escapeTool); 228 context.put("rdf", model.getGraph()); 229 230 context.put("model", model); 231 context.put("subjects", model.listSubjects()); 232 context.put("nodeany", ANY); 233 context.put("topic", subject); 234 context.put("uriInfo", uriInfo); 235 return context; 236 } 237 238 private Template getTemplate(final Model rdf, final Node subject, 239 final List<Annotation> annotations) { 240 241 Optional<Template> template = annotations.stream() 242 .filter(x -> x instanceof HtmlTemplate) 243 .map(x -> ((HtmlTemplate) x).value()) 244 .filter(templatesMap::containsKey) 245 .map(templatesMap::get) 246 .findFirst(); 247 248 if (!template.isPresent()) { 249 LOGGER.trace("Attempting to discover mixin types of node for resource in question: {}", subject); 250 template = rdf.listObjectsOfProperty(createResource(subject.getURI()), 251 createProperty(getRDFNamespaceForJcrNamespace(JCR_NAMESPACE) + 252 "mixinTypes")) 253 .toList().stream() 254 .map(x -> x.asLiteral().getLexicalForm()) 255 .filter(templatesMap::containsKey) 256 .map(templatesMap::get) 257 .findFirst(); 258 } 259 260 if (template.isPresent()) { 261 LOGGER.debug("Choosing template: {}", template.get().getName()); 262 return template.get(); 263 } else { 264 LOGGER.trace("Attempting to discover primary type of node for resource in question: {}", subject); 265 return rdf.listObjectsOfProperty(createResource(subject.getURI()), 266 createProperty(getRDFNamespaceForJcrNamespace(JCR_NAMESPACE) + 267 "primaryType")) 268 .toList().stream() 269 .map(x -> x.asLiteral().getLexicalForm()) 270 .filter(templatesMap::containsKey) 271 .map(templatesMap::get) 272 .findFirst() 273 .orElse(templatesMap.get("node")); 274 } 275 } 276 277 @Override 278 public boolean isWriteable(final Class<?> type, final Type genericType, 279 final Annotation[] annotations, final MediaType mediaType) { 280 LOGGER.debug( 281 "Checking to see if type: {} is serializable to mimeType: {}", 282 type.getName(), mediaType); 283 return (mediaType.equals(TEXT_HTML_TYPE) || mediaType 284 .equals(APPLICATION_XHTML_XML_TYPE)) 285 && RdfStream.class.isAssignableFrom(type); 286 } 287 288 @Override 289 public long getSize(final RdfStream t, final Class<?> type, 290 final Type genericType, final Annotation[] annotations, 291 final MediaType mediaType) { 292 // we don't know in advance how large the result might be 293 return -1; 294 } 295 296 private void addTemplate(final String primaryNodeTypeName, final String templateNodeTypeName, 297 final ImmutableMap.Builder<String, Template> templatesMapBuilder) { 298 final String templateLocation = getTemplateLocation(templateNodeTypeName); 299 final Template template = 300 velocity.getTemplate(templateLocation); 301 template.setName(templateLocation); 302 LOGGER.debug("Found template: {}", templateLocation); 303 templatesMapBuilder.put(primaryNodeTypeName, template); 304 LOGGER.debug("which we will use for nodes with primary type: {}", 305 primaryNodeTypeName); 306 } 307 308 private static String getTemplateLocation(final String nodeTypeName) { 309 return templatesLocation + "/" + 310 nodeTypeName.replace(':', '-') + templateFilenameExtension; 311 } 312}