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.utils.ContentDigest; 031import org.fcrepo.kernel.api.utils.FixityResult; 032import org.fcrepo.kernel.api.utils.iterators.RdfStream; 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.api.FedoraJcrTypes.HAS_MIME_TYPE; 053import static org.fcrepo.kernel.api.FedoraJcrTypes.FILENAME; 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 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.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 * (non-Javadoc) 122 * @see org.fcrepo.kernel.api.models.FedoraBinary#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.api.models.FedoraBinary#setContent(java.io.InputStream, 138 * java.lang.String, java.net.URI, java.lang.String, 139 * org.fcrepo.kernel.api.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(HAS_MIME_TYPE, contentType); 156 } 157 158 if (originalFileName != null) { 159 contentNode.setProperty(FILENAME, 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.api.models.FedoraBinary#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.api.models.FedoraBinary#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.api.models.FedoraBinary#getMimeType() 243 */ 244 @Override 245 public String getMimeType() { 246 try { 247 if (hasProperty(HAS_MIME_TYPE)) { 248 return getProperty(HAS_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.api.models.FedoraBinary#getFilename() 259 */ 260 @Override 261 public String getFilename() { 262 try { 263 if (hasProperty(FILENAME)) { 264 return getProperty(FILENAME).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 LOGGER.debug("Checking resource: " + getPath()); 287 288 final String algorithm = ContentDigest.getAlgorithm(digestUri); 289 290 final long contentSize = size < 0 ? getBinaryContent().getSize() : size; 291 292 final Collection<FixityResult> fixityResults 293 = CacheEntryFactory.forProperty(getProperty(JCR_DATA)).checkFixity(algorithm); 294 295 return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, contentSize); 296 } catch (final RepositoryException e) { 297 throw new RepositoryRuntimeException(e); 298 } 299 } 300 301 /** 302 * When deleting the binary, we also need to clean up the description document. 303 */ 304 @Override 305 public void delete() { 306 final NonRdfSourceDescription description = getDescription(); 307 308 super.delete(); 309 310 description.delete(); 311 } 312 313 @Override 314 public Version getBaseVersion() { 315 return getDescription().getBaseVersion(); 316 } 317 318 private static void decorateContentNode(final Node contentNode) throws RepositoryException { 319 if (contentNode == null) { 320 LOGGER.warn("{} node appears to be null!", JCR_CONTENT); 321 return; 322 } 323 if (contentNode.canAddMixin(FEDORA_BINARY)) { 324 contentNode.addMixin(FEDORA_BINARY); 325 } 326 327 if (contentNode.hasProperty(JCR_DATA)) { 328 final Property dataProperty = contentNode.getProperty(JCR_DATA); 329 final Binary binary = (Binary) dataProperty.getBinary(); 330 final String dsChecksum = binary.getHexHash(); 331 332 contentSizeHistogram.update(dataProperty.getLength()); 333 334 contentNode.setProperty(CONTENT_SIZE, dataProperty.getLength()); 335 contentNode.setProperty(CONTENT_DIGEST, ContentDigest.asURI("SHA-1", dsChecksum).toString()); 336 337 LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath()); 338 } 339 } 340 341 /* 342 * (non-Javadoc) 343 * @see org.fcrepo.kernel.api.models.FedoraResource#getVersionHistory() 344 */ 345 @Override 346 public VersionHistory getVersionHistory() { 347 try { 348 return getSession().getWorkspace().getVersionManager().getVersionHistory(getDescription().getPath()); 349 } catch (final RepositoryException e) { 350 throw new RepositoryRuntimeException(e); 351 } 352 } 353 354 355 @Override 356 public boolean isVersioned() { 357 return getDescription().isVersioned(); 358 } 359 360 @Override 361 public void enableVersioning() { 362 super.enableVersioning(); 363 getDescription().enableVersioning(); 364 } 365 366 @Override 367 public void disableVersioning() { 368 super.disableVersioning(); 369 getDescription().disableVersioning(); 370 } 371 372 /** 373 * Check if the given node is a Fedora binary 374 * @param node the given node 375 * @return whether the given node is a Fedora binary 376 */ 377 public static boolean hasMixin(final Node node) { 378 return isFedoraBinary.test(node); 379 } 380}