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