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