001/** 002 * Copyright 2014 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.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.impl.utils.FedoraTypesUtils.isFedoraBinary; 053import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 054import static org.modeshape.jcr.api.JcrConstants.JCR_DATA; 055import static org.modeshape.jcr.api.JcrConstants.JCR_MIME_TYPE; 056import static org.slf4j.LoggerFactory.getLogger; 057 058/** 059 * @author cabeer 060 * @since 9/19/14 061 */ 062public class FedoraBinaryImpl extends FedoraResourceImpl implements FedoraBinary { 063 064 private static final Logger LOGGER = getLogger(FedoraBinaryImpl.class); 065 066 067 static final RegistryService registryService = RegistryService.getInstance(); 068 static final Counter fixityCheckCounter 069 = registryService.getMetrics().counter(name(FedoraBinary.class, "fixity-check-counter")); 070 071 static final Timer timer = registryService.getMetrics().timer( 072 name(NonRdfSourceDescription.class, "fixity-check-time")); 073 074 static final Histogram contentSizeHistogram = 075 registryService.getMetrics().histogram(name(FedoraBinary.class, "content-size")); 076 077 /** 078 * Wrap an existing Node as a Fedora Binary 079 * @param node 080 */ 081 public FedoraBinaryImpl(final Node node) { 082 super(node); 083 084 if (node.isNew()) { 085 initializeNewBinaryProperties(); 086 } 087 } 088 089 private void initializeNewBinaryProperties() { 090 try { 091 decorateContentNode(node); 092 } catch (RepositoryException e) { 093 LOGGER.warn("Count not decorate {} with FedoraBinary properties: {}", node, e); 094 } 095 } 096 097 @Override 098 public NonRdfSourceDescription getDescription() { 099 try { 100 return new NonRdfSourceDescriptionImpl(getNode().getParent()); 101 } catch (final RepositoryException e) { 102 throw new RepositoryRuntimeException(e); 103 } 104 } 105 106 /* 107 * (non-Javadoc) 108 * @see org.fcrepo.kernel.Datastream#getContent() 109 */ 110 @Override 111 public InputStream getContent() { 112 try { 113 return getBinaryContent().getStream(); 114 } catch (final RepositoryException e) { 115 throw new RepositoryRuntimeException(e); 116 } 117 } 118 119 /* 120 * (non-Javadoc) 121 * @see org.fcrepo.kernel.Datastream#getBinaryContent() 122 */ 123 @Override 124 public 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.Datastream#setContent(java.io.InputStream, 137 * java.lang.String, java.net.URI, java.lang.String, 138 * org.fcrepo.kernel.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(JCR_MIME_TYPE, contentType); 155 } 156 157 if (originalFileName != null) { 158 contentNode.setProperty(PREMIS_FILE_NAME, 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(node); 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 198 LOGGER.debug("Created data property at path: {}", dataProperty.getPath()); 199 200 } catch (final RepositoryException e) { 201 throw new RepositoryRuntimeException(e); 202 } 203 } 204 205 /* 206 * (non-Javadoc) 207 * @see org.fcrepo.kernel.Datastream#getContentSize() 208 */ 209 @Override 210 public long getContentSize() { 211 try { 212 if (hasProperty(CONTENT_SIZE)) { 213 return getProperty(CONTENT_SIZE).getLong(); 214 } 215 } catch (final RepositoryException e) { 216 LOGGER.info("Could not get contentSize(): {}", e.getMessage()); 217 } 218 219 return -1L; 220 } 221 222 /* 223 * (non-Javadoc) 224 * @see org.fcrepo.kernel.Datastream#getContentDigest() 225 */ 226 @Override 227 public URI getContentDigest() { 228 try { 229 if (hasProperty(CONTENT_DIGEST)) { 230 return new URI(getProperty(CONTENT_DIGEST).getString()); 231 } 232 } catch (final RepositoryException | URISyntaxException e) { 233 LOGGER.info("Could not get content digest: {}", e.getMessage()); 234 } 235 236 return ContentDigest.missingChecksum(); 237 } 238 239 /* 240 * (non-Javadoc) 241 * @see org.fcrepo.kernel.Datastream#getMimeType() 242 */ 243 @Override 244 public String getMimeType() { 245 try { 246 if (hasProperty(JCR_MIME_TYPE)) { 247 return getProperty(JCR_MIME_TYPE).getString(); 248 } 249 return "application/octet-stream"; 250 } catch (final RepositoryException e) { 251 throw new RepositoryRuntimeException(e); 252 } 253 } 254 255 /* 256 * (non-Javadoc) 257 * @see org.fcrepo.kernel.Datastream#getFilename() 258 */ 259 @Override 260 public String getFilename() { 261 try { 262 if (hasProperty(PREMIS_FILE_NAME)) { 263 return getProperty(PREMIS_FILE_NAME).getString(); 264 } 265 return node.getParent().getName(); 266 } catch (final RepositoryException e) { 267 throw new RepositoryRuntimeException(e); 268 } 269 } 270 271 @Override 272 public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator) { 273 return getFixity(idTranslator, getContentDigest(), getContentSize()); 274 } 275 276 @Override 277 public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator, 278 final URI digestUri, 279 final long size) { 280 281 fixityCheckCounter.inc(); 282 283 try (final Timer.Context context = timer.time()) { 284 285 final Repository repo = node.getSession().getRepository(); 286 LOGGER.debug("Checking resource: " + getPath()); 287 288 final String algorithm = ContentDigest.getAlgorithm(digestUri); 289 290 final Collection<FixityResult> fixityResults 291 = CacheEntryFactory.forProperty(repo, getProperty(JCR_DATA)).checkFixity(algorithm); 292 293 return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, size); 294 } catch (final RepositoryException e) { 295 throw new RepositoryRuntimeException(e); 296 } 297 } 298 299 /** 300 * When deleting the binary, we also need to clean up the description document. 301 */ 302 @Override 303 public void delete() { 304 final NonRdfSourceDescription description = getDescription(); 305 306 super.delete(); 307 308 description.delete(); 309 } 310 311 @Override 312 public void addVersionLabel(final String label) { 313 getDescription().addVersionLabel(label); 314 } 315 316 private static void decorateContentNode(final Node contentNode) throws RepositoryException { 317 if (contentNode == null) { 318 LOGGER.warn("{} node appears to be null!", JCR_CONTENT); 319 return; 320 } 321 if (contentNode.canAddMixin(FEDORA_BINARY)) { 322 contentNode.addMixin(FEDORA_BINARY); 323 } 324 325 if (contentNode.hasProperty(JCR_DATA)) { 326 final Property dataProperty = contentNode.getProperty(JCR_DATA); 327 final Binary binary = (Binary) dataProperty.getBinary(); 328 final String dsChecksum = binary.getHexHash(); 329 330 contentSizeHistogram.update(dataProperty.getLength()); 331 332 contentNode.setProperty(CONTENT_SIZE, dataProperty.getLength()); 333 contentNode.setProperty(CONTENT_DIGEST, ContentDigest.asURI("SHA-1", dsChecksum).toString()); 334 335 LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath()); 336 } 337 } 338 339 /* 340 * (non-Javadoc) 341 * @see org.fcrepo.kernel.models.FedoraResource#getVersionHistory() 342 */ 343 @Override 344 public VersionHistory getVersionHistory() { 345 try { 346 return getSession().getWorkspace().getVersionManager().getVersionHistory(getDescription().getPath()); 347 } catch (final RepositoryException e) { 348 throw new RepositoryRuntimeException(e); 349 } 350 } 351 352 353 @Override 354 public boolean isVersioned() { 355 return getDescription().isVersioned(); 356 } 357 358 @Override 359 public void enableVersioning() { 360 super.enableVersioning(); 361 getDescription().enableVersioning(); 362 } 363 364 @Override 365 public void disableVersioning() { 366 super.disableVersioning(); 367 getDescription().disableVersioning(); 368 } 369 370 /** 371 * Check if the given node is a Fedora binary 372 * @param node 373 * @return 374 */ 375 public static boolean hasMixin(final Node node) { 376 return isFedoraBinary.apply(node); 377 } 378}