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.kernel.modeshape; 019 020import com.codahale.metrics.Counter; 021import com.codahale.metrics.Histogram; 022import com.codahale.metrics.Timer; 023import com.hp.hpl.jena.rdf.model.Resource; 024import org.fcrepo.kernel.api.exception.InvalidChecksumException; 025import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; 026import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 027import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 028import org.fcrepo.kernel.api.models.NonRdfSourceDescription; 029import org.fcrepo.kernel.api.models.FedoraBinary; 030import org.fcrepo.kernel.api.models.FedoraResource; 031import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint; 032import org.fcrepo.kernel.api.RdfStream; 033import org.fcrepo.kernel.api.utils.ContentDigest; 034import org.fcrepo.kernel.api.utils.FixityResult; 035import org.fcrepo.kernel.modeshape.rdf.impl.FixityRdfContext; 036import org.fcrepo.kernel.modeshape.utils.impl.CacheEntryFactory; 037import org.fcrepo.metrics.RegistryService; 038import org.modeshape.jcr.api.Binary; 039import org.modeshape.jcr.api.ValueFactory; 040import org.slf4j.Logger; 041 042import javax.jcr.Node; 043import javax.jcr.PathNotFoundException; 044import javax.jcr.Property; 045import javax.jcr.RepositoryException; 046import javax.jcr.version.Version; 047import javax.jcr.version.VersionHistory; 048import java.io.InputStream; 049import java.net.URI; 050import java.net.URISyntaxException; 051import java.util.Collection; 052 053import static com.codahale.metrics.MetricRegistry.name; 054import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isFedoraBinary; 055import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 056import static org.modeshape.jcr.api.JcrConstants.JCR_DATA; 057import static org.slf4j.LoggerFactory.getLogger; 058 059/** 060 * @author cabeer 061 * @since 9/19/14 062 */ 063public class FedoraBinaryImpl extends FedoraResourceImpl implements FedoraBinary { 064 065 private static final Logger LOGGER = getLogger(FedoraBinaryImpl.class); 066 067 068 static final RegistryService registryService = RegistryService.getInstance(); 069 static final Counter fixityCheckCounter 070 = registryService.getMetrics().counter(name(FedoraBinary.class, "fixity-check-counter")); 071 072 static final Timer timer = registryService.getMetrics().timer( 073 name(NonRdfSourceDescription.class, "fixity-check-time")); 074 075 static final Histogram contentSizeHistogram = 076 registryService.getMetrics().histogram(name(FedoraBinary.class, "content-size")); 077 078 /** 079 * Wrap an existing Node as a Fedora Binary 080 * @param node the node 081 */ 082 public FedoraBinaryImpl(final Node node) { 083 super(node); 084 085 if (node.isNew()) { 086 initializeNewBinaryProperties(); 087 } 088 } 089 090 private void initializeNewBinaryProperties() { 091 try { 092 decorateContentNode(node); 093 } catch (final RepositoryException e) { 094 LOGGER.warn("Count not decorate {} with FedoraBinary properties: {}", node, e); 095 } 096 } 097 098 @Override 099 public FedoraResource getDescription() { 100 try { 101 return new NonRdfSourceDescriptionImpl(getNode().getParent()); 102 } catch (final RepositoryException e) { 103 throw new RepositoryRuntimeException(e); 104 } 105 } 106 107 /* 108 * (non-Javadoc) 109 * @see org.fcrepo.kernel.api.models.FedoraBinary#getContent() 110 */ 111 @Override 112 public InputStream getContent() { 113 try { 114 return getBinaryContent().getStream(); 115 } catch (final RepositoryException e) { 116 throw new RepositoryRuntimeException(e); 117 } 118 } 119 120 /** 121 * Retrieve the JCR Binary object 122 * @return a JCR-wrapped Binary object 123 */ 124 private javax.jcr.Binary getBinaryContent() { 125 try { 126 return getProperty(JCR_DATA).getBinary(); 127 } catch (final PathNotFoundException e) { 128 throw new PathNotFoundRuntimeException(e); 129 } catch (final RepositoryException e) { 130 throw new RepositoryRuntimeException(e); 131 } 132 } 133 134 /* 135 * (non-Javadoc) 136 * @see org.fcrepo.kernel.api.models.FedoraBinary#setContent(java.io.InputStream, 137 * java.lang.String, java.net.URI, java.lang.String, 138 * org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint) 139 */ 140 @Override 141 public void setContent(final InputStream content, final String contentType, 142 final URI checksum, final String originalFileName, 143 final StoragePolicyDecisionPoint storagePolicyDecisionPoint) 144 throws InvalidChecksumException { 145 146 try { 147 final Node contentNode = getNode(); 148 149 if (contentNode.canAddMixin(FEDORA_BINARY)) { 150 contentNode.addMixin(FEDORA_BINARY); 151 } 152 153 if (contentType != null) { 154 contentNode.setProperty(HAS_MIME_TYPE, contentType); 155 } 156 157 if (originalFileName != null) { 158 contentNode.setProperty(FILENAME, originalFileName); 159 } 160 161 LOGGER.debug("Created content node at path: {}", contentNode.getPath()); 162 163 String hint = null; 164 165 if (storagePolicyDecisionPoint != null) { 166 hint = storagePolicyDecisionPoint.evaluatePolicies(this); 167 } 168 final ValueFactory modevf = 169 (ValueFactory) node.getSession().getValueFactory(); 170 final Binary binary = modevf.createBinary(content, hint); 171 172 /* 173 * This next line of code deserves explanation. If we chose for the 174 * simpler line: Property dataProperty = 175 * contentNode.setProperty(JCR_DATA, requestBodyStream); then the JCR 176 * would not block on the stream's completion, and we would return to 177 * the requester before the mutation to the repo had actually completed. 178 * So instead we use createBinary(requestBodyStream), because its 179 * contract specifies: "The passed InputStream is closed before this 180 * method returns either normally or because of an exception." which 181 * lets us block and not return until the job is done! The simpler code 182 * may still be useful to us for an asynchronous method that we develop 183 * later. 184 */ 185 final Property dataProperty = contentNode.setProperty(JCR_DATA, binary); 186 187 final String dsChecksum = binary.getHexHash(); 188 final URI uriChecksumString = ContentDigest.asURI("SHA-1", dsChecksum); 189 if (checksum != null && 190 !checksum.equals(uriChecksumString)) { 191 LOGGER.debug("Failed checksum test"); 192 throw new InvalidChecksumException("Checksum Mismatch of " + 193 uriChecksumString + " and " + checksum); 194 } 195 196 decorateContentNode(contentNode); 197 touch(); 198 ((FedoraResourceImpl) getDescription()).touch(); 199 200 LOGGER.debug("Created data property at path: {}", dataProperty.getPath()); 201 202 } catch (final RepositoryException e) { 203 throw new RepositoryRuntimeException(e); 204 } 205 } 206 207 /* 208 * (non-Javadoc) 209 * @see org.fcrepo.kernel.api.models.FedoraBinary#getContentSize() 210 */ 211 @Override 212 public long getContentSize() { 213 try { 214 if (hasProperty(CONTENT_SIZE)) { 215 return getProperty(CONTENT_SIZE).getLong(); 216 } 217 } catch (final RepositoryException e) { 218 LOGGER.info("Could not get contentSize(): {}", e.getMessage()); 219 } 220 221 return -1L; 222 } 223 224 /* 225 * (non-Javadoc) 226 * @see org.fcrepo.kernel.api.models.FedoraBinary#getContentDigest() 227 */ 228 @Override 229 public URI getContentDigest() { 230 try { 231 if (hasProperty(CONTENT_DIGEST)) { 232 return new URI(getProperty(CONTENT_DIGEST).getString()); 233 } 234 } catch (final RepositoryException | URISyntaxException e) { 235 LOGGER.info("Could not get content digest: {}", e.getMessage()); 236 } 237 238 return ContentDigest.missingChecksum(); 239 } 240 241 /* 242 * (non-Javadoc) 243 * @see org.fcrepo.kernel.api.models.FedoraBinary#getMimeType() 244 */ 245 @Override 246 public String getMimeType() { 247 try { 248 if (hasProperty(HAS_MIME_TYPE)) { 249 return getProperty(HAS_MIME_TYPE).getString(); 250 } 251 return "application/octet-stream"; 252 } catch (final RepositoryException e) { 253 throw new RepositoryRuntimeException(e); 254 } 255 } 256 257 /* 258 * (non-Javadoc) 259 * @see org.fcrepo.kernel.api.models.FedoraBinary#getFilename() 260 */ 261 @Override 262 public String getFilename() { 263 try { 264 if (hasProperty(FILENAME)) { 265 return getProperty(FILENAME).getString(); 266 } 267 return node.getParent().getName(); 268 } catch (final RepositoryException e) { 269 throw new RepositoryRuntimeException(e); 270 } 271 } 272 273 @Override 274 public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator) { 275 return getFixity(idTranslator, getContentDigest(), getContentSize()); 276 } 277 278 @Override 279 public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator, 280 final URI digestUri, 281 final long size) { 282 283 fixityCheckCounter.inc(); 284 285 try (final Timer.Context context = timer.time()) { 286 287 LOGGER.debug("Checking resource: " + getPath()); 288 289 final String algorithm = ContentDigest.getAlgorithm(digestUri); 290 291 final long contentSize = size < 0 ? getBinaryContent().getSize() : size; 292 293 final Collection<FixityResult> fixityResults 294 = CacheEntryFactory.forProperty(getProperty(JCR_DATA)).checkFixity(algorithm); 295 296 return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, contentSize); 297 } catch (final RepositoryException e) { 298 throw new RepositoryRuntimeException(e); 299 } 300 } 301 302 /** 303 * When deleting the binary, we also need to clean up the description document. 304 */ 305 @Override 306 public void delete() { 307 final FedoraResource description = getDescription(); 308 309 super.delete(); 310 311 description.delete(); 312 } 313 314 @Override 315 public Version getBaseVersion() { 316 return getDescription().getBaseVersion(); 317 } 318 319 private static void decorateContentNode(final Node contentNode) throws RepositoryException { 320 if (contentNode == null) { 321 LOGGER.warn("{} node appears to be null!", JCR_CONTENT); 322 return; 323 } 324 if (contentNode.canAddMixin(FEDORA_BINARY)) { 325 contentNode.addMixin(FEDORA_BINARY); 326 } 327 328 if (contentNode.hasProperty(JCR_DATA)) { 329 final Property dataProperty = contentNode.getProperty(JCR_DATA); 330 final Binary binary = (Binary) dataProperty.getBinary(); 331 final String dsChecksum = binary.getHexHash(); 332 333 contentSizeHistogram.update(dataProperty.getLength()); 334 335 contentNode.setProperty(CONTENT_SIZE, dataProperty.getLength()); 336 contentNode.setProperty(CONTENT_DIGEST, ContentDigest.asURI("SHA-1", dsChecksum).toString()); 337 338 LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath()); 339 } 340 } 341 342 /* 343 * (non-Javadoc) 344 * @see org.fcrepo.kernel.api.models.FedoraResource#getVersionHistory() 345 */ 346 @Override 347 public VersionHistory getVersionHistory() { 348 try { 349 return getVersionManager().getVersionHistory(getDescription().getPath()); 350 } catch (final RepositoryException e) { 351 throw new RepositoryRuntimeException(e); 352 } 353 } 354 355 356 @Override 357 public boolean isVersioned() { 358 return getDescription().isVersioned(); 359 } 360 361 @Override 362 public void enableVersioning() { 363 super.enableVersioning(); 364 getDescription().enableVersioning(); 365 } 366 367 @Override 368 public void disableVersioning() { 369 super.disableVersioning(); 370 getDescription().disableVersioning(); 371 } 372 373 /** 374 * Check if the given node is a Fedora binary 375 * @param node the given node 376 * @return whether the given node is a Fedora binary 377 */ 378 public static boolean hasMixin(final Node node) { 379 return isFedoraBinary.test(node); 380 } 381}