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 static org.fcrepo.kernel.api.utils.ContentDigest.DIGEST_ALGORITHM.SHA1; 021import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; 022import static org.modeshape.jcr.api.JcrConstants.JCR_DATA; 023import static org.slf4j.LoggerFactory.getLogger; 024 025import java.io.InputStream; 026import java.net.URI; 027import java.util.Collection; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.Map; 031import java.util.stream.Collectors; 032 033import javax.jcr.Node; 034import javax.jcr.PathNotFoundException; 035import javax.jcr.Property; 036import javax.jcr.RepositoryException; 037 038import org.apache.jena.rdf.model.Resource; 039import org.fcrepo.kernel.api.RdfStream; 040import org.fcrepo.kernel.api.exception.InvalidChecksumException; 041import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; 042import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 043import org.fcrepo.kernel.api.exception.UnsupportedAlgorithmException; 044import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 045import org.fcrepo.kernel.api.models.FedoraResource; 046import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint; 047import org.fcrepo.kernel.api.utils.CacheEntry; 048import org.fcrepo.kernel.api.utils.ContentDigest; 049import org.fcrepo.kernel.api.utils.FixityResult; 050import org.fcrepo.kernel.modeshape.rdf.impl.FixityRdfContext; 051import org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils; 052import org.fcrepo.kernel.modeshape.utils.impl.CacheEntryFactory; 053import org.modeshape.jcr.api.Binary; 054import org.modeshape.jcr.api.ValueFactory; 055import org.slf4j.Logger; 056 057/** 058 * Fedora Binary stored internally in modeshape 059 * 060 * @author bbpennel 061 */ 062public class InternalFedoraBinary extends AbstractFedoraBinary { 063 064 private static final Logger LOGGER = getLogger(InternalFedoraBinary.class); 065 066 /** 067 * Construct InternalFedoraBinary 068 * 069 * @param node node 070 */ 071 public InternalFedoraBinary(final Node node) { 072 super(node); 073 } 074 075 /* 076 * (non-Javadoc) 077 * @see org.fcrepo.kernel.api.models.FedoraBinary#getContent() 078 */ 079 @Override 080 public InputStream getContent() { 081 try { 082 return getBinaryContent().getStream(); 083 } catch (final RepositoryException e) { 084 throw new RepositoryRuntimeException(e); 085 } 086 } 087 088 /** 089 * Retrieve the JCR Binary object 090 * 091 * @return a JCR-wrapped Binary object 092 */ 093 private javax.jcr.Binary getBinaryContent() { 094 try { 095 return getProperty(JCR_DATA).getBinary(); 096 } catch (final PathNotFoundException e) { 097 throw new PathNotFoundRuntimeException(e); 098 } catch (final RepositoryException e) { 099 throw new RepositoryRuntimeException(e); 100 } 101 } 102 103 @Override 104 public void setExternalContent(final String contentType, final Collection<URI> checksums, 105 final String originalFileName, final String externalHandling, 106 final String externalUrl) { 107 throw new UnsupportedOperationException("Cannot call setExternalContent from this context"); 108 } 109 110 /* 111 * (non-Javadoc) 112 * @see org.fcrepo.kernel.api.models.FedoraBinary#setContent(java.io.InputStream, java.lang.String, java.net.URI, 113 * java.lang.String, org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint) 114 */ 115 @Override 116 public void setContent(final InputStream content, final String contentType, 117 final Collection<URI> checksums, final String originalFileName, 118 final StoragePolicyDecisionPoint storagePolicyDecisionPoint) 119 throws InvalidChecksumException { 120 121 try { 122 123 124 final Node dsNode = getNode(); 125 126 LOGGER.debug("Created content node at path: {}", dsNode.getPath()); 127 128 String hint = null; 129 130 if (storagePolicyDecisionPoint != null) { 131 hint = storagePolicyDecisionPoint.evaluatePolicies(this); 132 } 133 final ValueFactory modevf = 134 (ValueFactory) node.getSession().getValueFactory(); 135 final Binary binary = modevf.createBinary(content, hint); 136 137 /* 138 * This next line of code deserves explanation. If we chose for the 139 * simpler line: Property dataProperty = 140 * contentNode.setProperty(JCR_DATA, requestBodyStream); then the JCR 141 * would not block on the stream's completion, and we would return to 142 * the requester before the mutation to the repo had actually completed. 143 * So instead we use createBinary(requestBodyStream), because its 144 * contract specifies: "The passed InputStream is closed before this 145 * method returns either normally or because of an exception." which 146 * lets us block and not return until the job is done! The simpler code 147 * may still be useful to us for an asynchronous method that we develop 148 * later. 149 */ 150 final Property dataProperty = dsNode.setProperty(JCR_DATA, binary); 151 152 // Ensure provided checksums are valid 153 final Collection<URI> nonNullChecksums = (null == checksums) ? new HashSet<>() : checksums; 154 verifyChecksums(nonNullChecksums, dataProperty); 155 156 final Node descNode = getDescriptionNodeOrNull(); 157 158 decorateContentNode(dsNode, descNode, nonNullChecksums); 159 FedoraTypesUtils.touch(dsNode); 160 161 if (descNode != null) { 162 descNode.setProperty(HAS_MIME_TYPE, contentType); 163 164 if (originalFileName != null) { 165 descNode.setProperty(FILENAME, originalFileName); 166 } 167 168 FedoraTypesUtils.touch(descNode); 169 } 170 171 LOGGER.debug("Created data property at path: {}", dataProperty.getPath()); 172 173 } catch (final RepositoryException e) { 174 throw new RepositoryRuntimeException(e); 175 } 176 } 177 178 /* 179 * (non-Javadoc) 180 * @see org.fcrepo.kernel.api.models.FedoraBinary#getMimeType() 181 */ 182 @Override 183 public String getMimeType() { 184 final String mimeType = getMimeTypeValue(); 185 if (mimeType == null) { 186 return "application/octet-stream"; 187 } else { 188 return mimeType; 189 } 190 } 191 192 /** 193 * This method ensures that the arg checksums are valid against the binary associated with the arg dataProperty. 194 * If one or more of the checksums are invalid, an InvalidChecksumException is thrown. 195 * 196 * @param checksums that the user provided 197 * @param dataProperty containing the binary against which the checksums will be verified 198 * @throws InvalidChecksumException on error 199 */ 200 private void verifyChecksums(final Collection<URI> checksums, final Property dataProperty) 201 throws InvalidChecksumException { 202 203 final Map<URI, URI> checksumErrors = new HashMap<>(); 204 205 // Loop through provided checksums validating against computed values 206 checksums.forEach(checksum -> { 207 final String algorithm = ContentDigest.getAlgorithm(checksum); 208 try { 209 // The case internally supported by ModeShape 210 if (algorithm.equals(SHA1.algorithm)) { 211 final String dsSHA1 = ((Binary) dataProperty.getBinary()).getHexHash(); 212 final URI dsSHA1Uri = ContentDigest.asURI(SHA1.algorithm, dsSHA1); 213 214 if (!dsSHA1Uri.equals(checksum)) { 215 LOGGER.debug("Failed checksum test"); 216 checksumErrors.put(checksum, dsSHA1Uri); 217 } 218 219 // The case that requires re-computing the checksum 220 } else { 221 final CacheEntry cacheEntry = CacheEntryFactory.forProperty(dataProperty); 222 cacheEntry.checkFixity(algorithm).stream().findFirst().ifPresent( 223 fixityResult -> { 224 if (!fixityResult.matches(checksum)) { 225 LOGGER.debug("Failed checksum test"); 226 checksumErrors.put(checksum, fixityResult.getComputedChecksum()); 227 } 228 }); 229 } 230 } catch (final RepositoryException e) { 231 throw new RepositoryRuntimeException(e); 232 } 233 }); 234 235 // Throw an exception if any checksum errors occurred 236 if (!checksumErrors.isEmpty()) { 237 final String template = "Checksum Mismatch of %1$s and %2$s\n"; 238 final StringBuilder error = new StringBuilder(); 239 checksumErrors.forEach((key, value) -> error.append(String.format(template, key, value))); 240 throw new InvalidChecksumException(error.toString()); 241 } 242 243 } 244 245 @Override 246 public RdfStream getFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator, 247 final URI digestUri, 248 final long size) { 249 250 try { 251 252 LOGGER.debug("Checking resource: {}" + getPath()); 253 254 final String algorithm = ContentDigest.getAlgorithm(digestUri); 255 256 final long contentSize = size < 0 ? getBinaryContent().getSize() : size; 257 258 final Collection<FixityResult> fixityResults = CacheEntryFactory.forProperty(getProperty(JCR_DATA)) 259 .checkFixity(algorithm); 260 261 return new FixityRdfContext(this, idTranslator, fixityResults, digestUri, contentSize); 262 } catch (final RepositoryException e) { 263 throw new RepositoryRuntimeException(e); 264 } 265 } 266 267 @Override 268 public Collection<URI> checkFixity(final IdentifierConverter<Resource, FedoraResource> idTranslator, 269 final Collection<String> algorithms) 270 throws UnsupportedAlgorithmException { 271 272 try { 273 274 LOGGER.debug("Checking resource: {}", getPath()); 275 return CacheEntryFactory.forProperty(getProperty(JCR_DATA)).checkFixity(algorithms); 276 } catch (final RepositoryException e) { 277 throw new RepositoryRuntimeException(e); 278 } 279 } 280 281 /** 282 * Add necessary information to node 283 * @param dsNode The target binary node to add information to 284 * @param descNode The description node associated with the binary node 285 * @param checksums The checksum information 286 * @throws RepositoryException on error 287 */ 288 private static void decorateContentNode(final Node dsNode, final Node descNode, final Collection<URI> checksums) 289 throws RepositoryException { 290 291 if (dsNode == null) { 292 LOGGER.warn("{} node appears to be null!", JCR_CONTENT); 293 return; 294 } 295 296 if (dsNode.hasProperty(JCR_DATA)) { 297 final Property dataProperty = dsNode.getProperty(JCR_DATA); 298 final Binary binary = (Binary) dataProperty.getBinary(); 299 final String dsChecksum = binary.getHexHash(); 300 301 checksums.add(ContentDigest.asURI(SHA1.algorithm, dsChecksum)); 302 303 final String[] checksumArray = new String[checksums.size()]; 304 checksums.stream().map(Object::toString).collect(Collectors.toSet()).toArray(checksumArray); 305 306 if (descNode != null) { 307 descNode.setProperty(CONTENT_DIGEST, checksumArray); 308 descNode.setProperty(CONTENT_SIZE, dataProperty.getLength()); 309 } 310 311 LOGGER.debug("Decorated data property at path: {}", dataProperty.getPath()); 312 } 313 } 314 315 @Override 316 public Boolean isRedirect() { 317 return false; 318 } 319 320 @Override 321 public Boolean isProxy() { 322 return false; 323 } 324 325}