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