org.sakaiproject.tool.assessment.facade.ItemHashUtil Maven / Gradle / Ivy
/**
* Copyright (c) 2005-2017 The Apereo Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://opensource.org/licenses/ecl2
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sakaiproject.tool.assessment.facade;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.FlushMode;
import org.hibernate.query.Query;
import org.hibernate.Session;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.exception.ServerOverloadException;
import org.sakaiproject.tool.assessment.data.ifc.assessment.AnswerIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.AttachmentIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemDataIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemMetaDataIfc;
import org.sakaiproject.tool.assessment.data.ifc.assessment.ItemTextIfc;
import org.sakaiproject.tool.assessment.data.ifc.shared.TypeIfc;
import org.sakaiproject.tool.assessment.services.assessment.AssessmentService;
import org.springframework.orm.hibernate5.HibernateTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import lombok.extern.slf4j.Slf4j;
/**
* For shared {@code Item} hashing functionality shared between "query" classes for items of
* any scope, e.g. published and un-published.
*/
@Slf4j
class ItemHashUtil {
static final String TOTAL_ITEM_COUNT_HQL = "total.item.count.hql";
static final String TOTAL_HASH_BACKFILLABLE_ITEM_COUNT_HQL = "total.hash.backfillable.item.count.hql";
static final String ALL_HASH_BACKFILLABLE_ITEM_IDS_HQL = "all.backfillable.item.ids.hql";
static final String ITEMS_BY_ID_HQL = "items.by.id.hql";
static final String ID_PARAMS_PLACEHOLDER = "{ID_PARAMS}";
private ContentHostingService contentHostingService;
private PlatformTransactionManager transactionManager;
/**
* Bit of a hack to allow reuse between {@link ItemFacadeQueries} and {@link PublishedItemFacadeQueries}.
* Arguments are rather arbitrary extension points to support what we happen to know are the differences
* between item and published item processing, as well as the common utilities/service dependencies.
*
* @param batchSize
* @param hqlQueries
* @param concreteType
* @param hashAndAssignCallback
* @param hibernateTemplate
* @return
*/
BackfillItemHashResult backfillItemHashes(int batchSize,
Map hqlQueries,
Class extends ItemDataIfc> concreteType,
Function hashAndAssignCallback,
HibernateTemplate hibernateTemplate) {
final long startTime = System.currentTimeMillis();
log.debug("Hash backfill starting for items of type [" + concreteType.getSimpleName() + "]");
if ( batchSize <= 0 ) {
batchSize = 100;
}
final int flushSize = batchSize;
final AtomicInteger totalItems = new AtomicInteger(0);
final AtomicInteger totalItemsNeedingBackfill = new AtomicInteger(0);
final AtomicInteger batchNumber = new AtomicInteger(0);
final AtomicInteger recordsRead = new AtomicInteger(0);
final AtomicInteger recordsUpdated = new AtomicInteger(0);
final Map hashingErrors = new TreeMap<>();
final Map otherErrors = new TreeMap<>();
final List batchElapsedTimes = new ArrayList<>();
// always needed as *printable* average per-batch timing value, so just store as string. and cache at this
// scope b/c we sometimes need to print a single calculation multiple times, e.g. in last batch and
// at method exit
final AtomicReference currentAvgBatchElapsedTime = new AtomicReference<>("0.00");
final AtomicBoolean areMoreItems = new AtomicBoolean(true);
// Get the item totals up front since a) we know any questions created while the job is running will be
// assigned hashes and thus won't need to be handled by the job and b) makes bookkeeping within the job much
// easier
hibernateTemplate.execute(session -> {
session.setDefaultReadOnly(true);
totalItems.set(countItems(hqlQueries, session));
totalItemsNeedingBackfill.set(countItemsNeedingHashBackfill(hqlQueries, session));
log.debug("Hash backfill required for [" + totalItemsNeedingBackfill + "] of [" + totalItems
+ "] items of type [" + concreteType.getSimpleName() + "]");
return null;
});
while (areMoreItems.get()) {
long batchStartTime = System.currentTimeMillis();
batchNumber.getAndIncrement();
final AtomicInteger itemsHashedInBatch = new AtomicInteger(0);
final AtomicInteger itemsReadInBatch = new AtomicInteger(0);
final AtomicReference failure = new AtomicReference<>(null);
// Idea here is a) avoid very long running transactions and b) avoid reading all items into memory
// and c) avoid weirdness, e.g. duplicate results, when paginating complex hibernate objects. So
// there's a per-batch transaction, and each batch re-runs the same two item lookup querys, one to
// get the list of IDs for the next page of items, and one to resolve those IDs to items
try {
new TransactionTemplate(transactionManager, requireNewTransaction()).execute(status -> {
hibernateTemplate.execute(session -> {
List itemsInBatch = null;
try { // resource cleanup block
session.setFlushMode(FlushMode.MANUAL);
try { // initial read block (failures here are fatal)
// set up the actual result set for this batch of items. use error count to skip over failed items
final List itemIds = itemIdsNeedingHashBackfill(hqlQueries, flushSize, hashingErrors.size(), session);
itemsInBatch = itemsById(itemIds, hqlQueries, session);
} catch (RuntimeException e) {
// Panic on failure to read counts and/or the actual items in the batch.
// Otherwise would potentially loop indefinitely since this design has no way way to
// skip this page of results.
log.error("Failed to read batch of hashable items. Giving up at record [" + recordsRead
+ "] of [" + totalItemsNeedingBackfill + "] Type: [" + concreteType.getSimpleName()
+ "]", e);
areMoreItems.set(false); // force overall loop to exit
throw e; // force txn to give up
}
for ( ItemDataIfc item : itemsInBatch ) {
recordsRead.getAndIncrement();
itemsReadInBatch.getAndIncrement();
// Assign the item's hash/es
try {
log.debug("Backfilling hash for item [" + recordsRead + "] of ["
+ totalItemsNeedingBackfill + "] Type: [" + concreteType.getSimpleName()
+ "] ID: [" + item.getItemId() + "]");
hashAndAssignCallback.apply(item);
itemsHashedInBatch.getAndIncrement();
} catch (Throwable t) {
// Failures considered ignorable here... probably some unexpected item state
// that prevented hash calculation.
//
// Re the log statement... yes, the caller probably logs exceptions, but likely
// without stack traces, and we'd like to advertise failures as quickly as possible,
// so we go ahead and emit an error log here.
log.error("Item hash calculation failed for item [" + recordsRead + "] of ["
+ totalItemsNeedingBackfill + "] Type: [" + concreteType.getSimpleName()
+ "] ID: [" + (item == null ? "?" : item.getItemId()) + "]", t);
hashingErrors.put(item.getItemId(), t);
}
}
if (itemsHashedInBatch.get() > 0) {
session.flush();
recordsUpdated.getAndAdd(itemsHashedInBatch.get());
}
areMoreItems.set(itemsInBatch.size() >= flushSize);
} finally {
quietlyClear(session); // potentially very large, so clear aggressively
}
return null;
}); // end session
return null;
}); // end transaction
} catch ( Throwable t ) {
// We're still in the loop over all batches, but something caused the current batch (and its
// transaction) to exit abnormally. Logging of both success and failure cases is quite detailed,
// and needs the same timing calcs, so is consolidated into the 'finally' block below.
failure.set(t);
otherErrors.put(batchNumber.get(), t);
} finally {
// Detailed batch-level reporting
final long batchElapsed = (System.currentTimeMillis() - batchStartTime);
batchElapsedTimes.add(batchElapsed);
currentAvgBatchElapsedTime.set(new DecimalFormat("#.00")
.format(batchElapsedTimes.stream().collect(Collectors.averagingLong(l -> l))));
if (failure.get() == null) {
log.debug("Item hash backfill batch flushed to database. Type: ["
+ concreteType.getSimpleName() + "] Batch number: ["
+ batchNumber + "] Items attempted in batch: [" + itemsReadInBatch
+ "] Items succeeded in batch: [" + itemsHashedInBatch
+ "] Total items attempted: [" + recordsRead + "] Total items succeeded: ["
+ recordsUpdated + "] Total attemptable items: [" + totalItemsNeedingBackfill
+ "] Elapsed batch time: [" + batchElapsed
+ "ms] Avg time/batch: ["
+ currentAvgBatchElapsedTime + "ms]");
} else {
// yes, caller probably logs exceptions later, but probably without stack traces, and we'd
// like to advertise failures as quickly as possible, so we go ahead and emit an error log
// here.
log.error("Item hash backfill failed. Type: ["
+ concreteType.getSimpleName() + "] Batch number: ["
+ batchNumber + "] Items attempted in batch: [" + itemsReadInBatch
+ "] Items flushable (but failed) in batch: [" + itemsHashedInBatch
+ "] Total items attempted: [" + recordsRead + "] Total items succeeded: ["
+ recordsUpdated + "] Total attemptable items: [" + totalItemsNeedingBackfill
+ "] Elapsed batch time: [" + batchElapsed
+ "ms] Avg time/batch: ["
+ currentAvgBatchElapsedTime + "ms]", failure.get());
}
}
} // end loop over all batches
final long elapsedTime = System.currentTimeMillis() - startTime;
log.debug("Hash backfill completed for items of type [" + concreteType.getSimpleName()
+ "]. Total items attempted: [" + recordsRead
+ "] Total items succeeded: [" + recordsUpdated + "] Target attemptable items: ["
+ totalItemsNeedingBackfill + "] Total elapsed time: [" + elapsedTime
+ "ms] Total batches: [" + batchNumber + "] Avg time/batch: ["
+ currentAvgBatchElapsedTime + "ms]");
return new BackfillItemHashResult(elapsedTime, totalItems.get(), totalItemsNeedingBackfill.get(),
recordsRead.get(), recordsUpdated.get(), flushSize, hashingErrors, otherErrors);
}
private int countItems(Map hqlQueries, Session session) {
@SuppressWarnings("unchecked")
final List totalItemsResult = session
.createQuery(hqlQueries.get(TOTAL_ITEM_COUNT_HQL))
.setReadOnly(true).list();
return totalItemsResult.get(0);
}
private int countItemsNeedingHashBackfill(Map hqlQueries, Session session) {
@SuppressWarnings("unchecked")
final List totalItemsNeedingBackfillResult = session
.createQuery(hqlQueries.get(TOTAL_HASH_BACKFILLABLE_ITEM_COUNT_HQL))
.setReadOnly(true).list();
return totalItemsNeedingBackfillResult.get(0);
}
private List itemIdsNeedingHashBackfill(Map hqlQueries, int pageSize, int pageStart, Session session) {
return session
.createQuery(hqlQueries.get(ALL_HASH_BACKFILLABLE_ITEM_IDS_HQL))
.setFirstResult(pageStart)
.setMaxResults(pageSize)
.list();
}
private List itemsById(List itemIds, Map hqlQueries, Session session) {
if ( itemIds == null || itemIds.isEmpty() ) {
return new ArrayList<>(0);
}
final String paramPlaceholders = StringUtils.repeat("?", ",", itemIds.size());
final Query query = session.createQuery(hqlQueries.get(ITEMS_BY_ID_HQL).replace(ID_PARAMS_PLACEHOLDER, paramPlaceholders));
final AtomicInteger position = new AtomicInteger(0);
itemIds.forEach(id -> query.setParameter(position.getAndIncrement(), id));
return query.list();
}
private TransactionDefinition requireNewTransaction() {
return new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
private void quietlyClear(Session session) {
if ( session != null ) {
try {
session.clear();
} catch (Exception e) {
// nothing to do
}
}
}
String hashItem(ItemDataIfc item) throws NoSuchAlgorithmException, IOException, ServerOverloadException {
StringBuilder hashBase = hashBaseForItem(item);
return hashString(hashBase.toString());
}
String hashItemUnchecked(ItemDataIfc item) {
try {
return hashItem(item);
} catch ( RuntimeException e ) {
throw e;
} catch ( Exception e ) {
throw new RuntimeException(e);
}
}
StringBuilder hashBaseForItem(ItemDataIfc item) throws NoSuchAlgorithmException, IOException, ServerOverloadException {
StringBuilder hashBase = new StringBuilder();
hashBaseForItemCoreProperties(item, hashBase);
hashBaseForItemAttachments(item, hashBase);
hashBaseForItemAnswers(item, hashBase);
hashBaseForItemMetadata(item, hashBase);
return hashBase;
}
StringBuilder hashBaseForItemCoreProperties(ItemDataIfc item, StringBuilder into)
throws NoSuchAlgorithmException, IOException, ServerOverloadException {
into.append("TypeId:" + item.getTypeId() + "::");
if (item.getTypeId() == TypeIfc.EXTENDED_MATCHING_ITEMS) {
into.append(normalizeResourceUrls("ThemeText", item.getThemeText()))
.append(normalizeResourceUrls("LeadInText",item.getLeadInText()));
} else {
into.append(normalizeResourceUrls("ItemText",item.getText()))
.append(normalizeResourceUrls("Instruction",item.getInstruction()));
}
return into.append(normalizeResourceUrls("CorrectItemFeedback",item.getCorrectItemFeedback()))
.append(normalizeResourceUrls("IncorrectItemFeedback",item.getInCorrectItemFeedback()))
.append(normalizeResourceUrls("GeneralCorrectItemFeedback",item.getGeneralItemFeedback()))
.append(normalizeResourceUrls("Description",item.getDescription()));
}
StringBuilder hashBaseForItemAttachments(ItemDataIfc item, StringBuilder into)
throws NoSuchAlgorithmException, IOException, ServerOverloadException {
final AssessmentService service = new AssessmentService();
final List attachmentResourceIds = service.getItemResourceIdList(item);
if ( attachmentResourceIds == null ) {
return into;
}
return hashBaseForResourceIds(attachmentResourceIds, into);
}
StringBuilder hashBaseForItemAnswers(ItemDataIfc item, StringBuilder into)
throws NoSuchAlgorithmException, IOException, ServerOverloadException {
if (item.getTypeId() == TypeIfc.EXTENDED_MATCHING_ITEMS){ //EMI question is different
if (item.getIsAnswerOptionsSimple()){
for ( AnswerIfc answerIfc : item.getEmiAnswerOptions() ) {
into.append(normalizeResourceUrls("EmiLabel",answerIfc.getLabel()));
}
}
if (item.getIsAnswerOptionsRich()){
into.append(normalizeResourceUrls("EmiAnswerOptionsRichText",item.getEmiAnswerOptionsRichText()));
}
for ( ItemTextIfc itemTextIfc : item.getEmiQuestionAnswerCombinations() ) {
into.append(normalizeResourceUrls("EmiCorrectOptionLabels" , itemTextIfc.getEmiCorrectOptionLabels()));
into.append(normalizeResourceUrls("EmiSequence" , Long.toString(itemTextIfc.getSequence())));
into.append(normalizeResourceUrls("EmiText" , itemTextIfc.getText()));
if (itemTextIfc.getHasAttachment() && itemTextIfc.isEmiQuestionItemText()){
final List itemTextAttachmentIfcList =
itemTextIfc
.getItemTextAttachmentSet()
.stream()
.map(AttachmentIfc::getResourceId)
.collect(Collectors.toList());
into = hashBaseForResourceIds(itemTextAttachmentIfcList, into);
}
}
}else{
//We use the itemTextArraySorted and answerArraySorted to be sure we retrieve the same order.
final List itemTextArraySorted = item.getItemTextArraySorted();
for ( ItemTextIfc itemTextIfc : itemTextArraySorted ) {
if ((item.getTypeId().equals(TypeIfc.MATCHING))||(item.getTypeId().equals(TypeIfc.MATRIX_CHOICES_SURVEY))||(item.getTypeId().equals(TypeIfc.CALCULATED_QUESTION))||(item.getTypeId().equals(TypeIfc.IMAGEMAP_QUESTION))) {
into.append(normalizeResourceUrls("ItemTextAnswer", itemTextIfc.getText()));
}
if ((item.getTypeId() != TypeIfc.AUDIO_RECORDING)&&(item.getTypeId() != TypeIfc.FILE_UPLOAD)) {
final List answerArraySorted = itemTextIfc.getAnswerArraySorted();
for (AnswerIfc answerIfc : answerArraySorted) {
String getIsCorrect = "" + answerIfc.getIsCorrect();
if (getIsCorrect.equals("null")){
getIsCorrect = null;
}
into.append(normalizeResourceUrls("ItemTextAnswer", answerIfc.getText()))
.append(normalizeResourceUrls("CorrectAnswerFeedback", answerIfc.getCorrectAnswerFeedback()))
.append(normalizeResourceUrls("InCorrectAnswerFeedback", answerIfc.getInCorrectAnswerFeedback()))
.append(normalizeResourceUrls("GeneralAnswerFeedback", answerIfc.getGeneralAnswerFeedback()))
.append(normalizeResourceUrls("AnswerSequence" , "" + answerIfc.getSequence() ))
.append(normalizeResourceUrls("AnswerLabel" , answerIfc.getLabel()))
.append(normalizeResourceUrls("AnswerIsCorrect" , getIsCorrect));
}
}
}
}
return into;
}
StringBuilder hashBaseForResourceIds(List resourceIdList, StringBuilder into)
throws NoSuchAlgorithmException, IOException, ServerOverloadException {
// Sort the hashes, not the resources, b/c the only reasonable option for sorting resources
// is the resourceId field, but that is unreliable because a resource rename shouldn't have
// an impact on question hashing.
final List hashes = new ArrayList<>(resourceIdList.size());
for ( String resourceId : resourceIdList ) {
ContentResource file = null;
try {
contentHostingService.checkResource(resourceId);
file = contentHostingService.getResource(resourceId);
} catch (Exception e) {
// nothing to do, resource does not exist or we don't have access to it
log.debug("Failed to access resource by id " + resourceId, e);
}
if ( file != null ) {
// The 1L means "hash the first KB". The hash will also include the size of the entire file as a
// stringified long. We do this b/c we suppose than a file where the first KB hash and the length are
// the same are very likely the same file from a content perspective. We only hash the first KB for
// performance.
final String hash = hashResource(file, 1L);
if ( hash != null ) {
hashes.add(hash);
}
}
}
if (hashes.size()>0) {
return into.append("Resources:" + hashes.stream().sorted().collect(Collectors.joining()) + "::");
}else{
return into;
}
}
StringBuilder hashBaseForItemMetadata(ItemDataIfc item, StringBuilder into)
throws NoSuchAlgorithmException, IOException, ServerOverloadException {
return into.append(normalizeMetadataUrl(ItemMetaDataIfc.RANDOMIZE,item.getItemMetaDataByLabel(ItemMetaDataIfc.RANDOMIZE)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.REQUIRE_ALL_OK,item.getItemMetaDataByLabel(ItemMetaDataIfc.REQUIRE_ALL_OK)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.IMAGE_MAP_SRC,item.getItemMetaDataByLabel(ItemMetaDataIfc.IMAGE_MAP_SRC)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.CASE_SENSITIVE_FOR_FIB,item.getItemMetaDataByLabel(ItemMetaDataIfc.CASE_SENSITIVE_FOR_FIB)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.MUTUALLY_EXCLUSIVE_FOR_FIB,item.getItemMetaDataByLabel(ItemMetaDataIfc.MUTUALLY_EXCLUSIVE_FOR_FIB)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.IGNORE_SPACES_FOR_FIB,item.getItemMetaDataByLabel(ItemMetaDataIfc.IGNORE_SPACES_FOR_FIB)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.MCMS_PARTIAL_CREDIT,item.getItemMetaDataByLabel(ItemMetaDataIfc.MCMS_PARTIAL_CREDIT)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.FORCE_RANKING,item.getItemMetaDataByLabel(ItemMetaDataIfc.FORCE_RANKING)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.MX_SURVEY_RELATIVE_WIDTH,item.getItemMetaDataByLabel(ItemMetaDataIfc.MX_SURVEY_RELATIVE_WIDTH)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.ADD_COMMENT_MATRIX,item.getItemMetaDataByLabel(ItemMetaDataIfc.ADD_COMMENT_MATRIX)))
.append(normalizeResourceUrls(ItemMetaDataIfc.MX_SURVEY_QUESTION_COMMENTFIELD,item.getItemMetaDataByLabel(ItemMetaDataIfc.MX_SURVEY_QUESTION_COMMENTFIELD)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.PREDEFINED_SCALE,item.getItemMetaDataByLabel(ItemMetaDataIfc.PREDEFINED_SCALE)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.TIMEALLOWED,item.getItemMetaDataByLabel(ItemMetaDataIfc.TIMEALLOWED)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.NUMATTEMPTS,item.getItemMetaDataByLabel(ItemMetaDataIfc.NUMATTEMPTS)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.SCALENAME,item.getItemMetaDataByLabel(ItemMetaDataIfc.SCALENAME)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.ADD_TO_FAVORITES_MATRIX,item.getItemMetaDataByLabel(ItemMetaDataIfc.ADD_TO_FAVORITES_MATRIX)))
.append(normalizeMetadataUrl(ItemMetaDataIfc.IMAGE_MAP_ALT_TEXT,item.getItemMetaDataByLabel(ItemMetaDataIfc.IMAGE_MAP_ALT_TEXT)));
}
String hashResource(ContentResource cr, long lengthInKBToHash)
throws NoSuchAlgorithmException, IOException, ServerOverloadException {
if ( cr == null ) {
return null;
}
final String algorithm = "SHA-256";
// compute the digest using the MD5 algorithm
final MessageDigest md = MessageDigest.getInstance(algorithm);
//To improve performance, we will only hash some bytes of the file.
if (lengthInKBToHash<=0L){
lengthInKBToHash = Long.MAX_VALUE;
}
final InputStream fis = cr.streamContent();
try {
final byte[] buffer = new byte[1024];
int numRead;
long lengthToRead = 0;
do {
numRead = fis.read(buffer);
if (numRead > 0) {
md.update(buffer, 0, numRead);
lengthToRead += 1;
}
} while ((numRead != -1) && (lengthToRead < lengthInKBToHash));
} finally {
if ( fis != null ) {
try {
fis.close();
} catch ( Exception e ) {
//nothing to do
}
}
}
// Include the file length as a disambiguator for files which otherwise happen to contain the same bytes in the
// lengthInKBToHash range. We don't include the file name in the hash base because this might be a renamed copy
// of a file, in which case the name is a spurious disambiguator for our purposes.
md.update((""+cr.getContentLength()).getBytes("UTF-8"));
byte[] digest = md.digest();
return Base64.encodeBase64String(digest);
}
String hashString(String textToHash) throws IOException, NoSuchAlgorithmException {
// This code is copied from org.sakaiproject.user.impl.PasswordService.hash()
final String algorithm = "SHA-256";
// compute the digest using the SHA-256 algorithm
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(textToHash.getBytes("UTF-8"));
final String rv = Base64.encodeBase64String(digest);
return rv;
}
String normalizeResourceUrls(String label, String textToParse) throws IOException, NoSuchAlgorithmException, ServerOverloadException {
if (StringUtils.isNotEmpty(textToParse)) {
String siteContentPath = "/access/content/";
String startSrc = "src=\"";
String endSrc = "\"";
//search for all the substrings that are potential links to resources
//if contains "..getServerUrl()/access/content/" then it's a standard site content file
if (textToParse != null) {
int beginIndex = textToParse.indexOf(startSrc);
if (beginIndex > 0) {
String sakaiSiteResourcePath = ServerConfigurationService.getServerUrl() + siteContentPath;
// have to loop because there may be more than one site of origin for the content
while (beginIndex > 0) {
int correctionIndex = 0;
beginIndex = beginIndex + startSrc.length();
int endIndex = textToParse.indexOf(endSrc, beginIndex);
String resourceURL = textToParse.substring(beginIndex, endIndex);
//GET THE RESOURCE or at least check if valid
//if contains "..getServerUrl()/access/content/" then it's a standard site content file
if (resourceURL.contains(sakaiSiteResourcePath)) {
String cleanResourceURL = resourceURL.substring(sakaiSiteResourcePath.length() - 1);
final String resourceHash = hashBaseForResourceIds(Arrays.asList(cleanResourceURL), new StringBuilder()).toString();
if (StringUtils.isNotEmpty(resourceHash)) {
textToParse = textToParse.replace(resourceURL, resourceHash);
correctionIndex = resourceHash.length() - resourceURL.length();
} // else just leave the URL unmolested if we can't resolve it to a readable resource
}
beginIndex = textToParse.indexOf(startSrc, endIndex + correctionIndex);
} // end while
}
}
return label + ":" + textToParse + "::";
}else{
return "";
}
}
String normalizeMetadataUrl(String label, String textToParse) throws IOException, NoSuchAlgorithmException, ServerOverloadException {
String siteContentPath = "/access/content/";
if (textToParse != null) {
//GET THE RESOURCE or at least check if valid
//if contains "/access/content/" then it's a standard site content file
if (textToParse.startsWith(siteContentPath)){
final String resourceId = textToParse.substring(siteContentPath.length()-1);
final String resourceHash = hashBaseForResourceIds(Arrays.asList(resourceId), new StringBuilder()).toString();
if (StringUtils.isNotEmpty(resourceHash)) {
textToParse = resourceHash;
} // else just leave the URL unmolested if we can't resolve it to a readable resource
}
return label + ":" + textToParse + "::";
}else{
return "";
}
}
public void setContentHostingService(ContentHostingService contentHostingService) {
this.contentHostingService = contentHostingService;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy