package com.constellio.model.services.records; import static com.constellio.model.services.search.query.logical.LogicalSearchQueryOperators.from; import static com.constellio.model.services.search.query.logical.LogicalSearchQueryOperators.fromAllSchemasIn; import static com.constellio.model.utils.MaskUtils.format; import static net.jcores.CoreKeeper.$; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.joda.time.LocalDateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.constellio.data.dao.dto.records.OptimisticLockingResolution; import com.constellio.data.dao.dto.records.RecordDTO; import com.constellio.data.dao.dto.records.RecordDeltaDTO; import com.constellio.data.dao.dto.records.RecordsFlushing; import com.constellio.data.dao.dto.records.TransactionDTO; import com.constellio.data.dao.dto.records.TransactionResponseDTO; import com.constellio.data.dao.services.DataStoreTypesFactory; import com.constellio.data.dao.services.bigVault.RecordDaoException.NoSuchRecordWithId; import com.constellio.data.dao.services.bigVault.RecordDaoException.OptimisticLocking; import com.constellio.data.dao.services.bigVault.RecordDaoRuntimeException.RecordDaoRuntimeException_RecordsFlushingFailed; import com.constellio.data.dao.services.bigVault.RecordDaoRuntimeException.ReferenceToNonExistentIndex; import com.constellio.data.dao.services.idGenerator.UniqueIdGenerator; import com.constellio.data.dao.services.records.RecordDao; import com.constellio.data.dao.services.sequence.SequencesManager; import com.constellio.data.utils.Factory; import com.constellio.data.utils.LangUtils; import com.constellio.data.utils.TimeProvider; import com.constellio.model.entities.Taxonomy; import com.constellio.model.entities.batchprocess.BatchProcess; import com.constellio.model.entities.configs.SystemConfiguration; import com.constellio.model.entities.records.Record; import com.constellio.model.entities.records.RecordRuntimeException; import com.constellio.model.entities.records.RecordUpdateOptions; import com.constellio.model.entities.records.Transaction; import com.constellio.model.entities.records.TransactionRecordsReindexation; import com.constellio.model.entities.records.wrappers.Collection; import com.constellio.model.entities.records.wrappers.Group; import com.constellio.model.entities.records.wrappers.RecordWrapper; import com.constellio.model.entities.records.wrappers.User; import com.constellio.model.entities.schemas.ConfigProvider; import com.constellio.model.entities.schemas.Metadata; import com.constellio.model.entities.schemas.MetadataSchema; import com.constellio.model.entities.schemas.MetadataSchemaType; import com.constellio.model.entities.schemas.MetadataSchemaTypes; import com.constellio.model.entities.schemas.ModificationImpact; import com.constellio.model.entities.schemas.Schemas; import com.constellio.model.entities.schemas.entries.SequenceDataEntry; import com.constellio.model.entities.schemas.preparationSteps.CalculateMetadatasRecordPreparationStep; import com.constellio.model.entities.schemas.preparationSteps.RecordPreparationStep; import com.constellio.model.entities.schemas.preparationSteps.SequenceRecordPreparationStep; import com.constellio.model.entities.schemas.preparationSteps.UpdateCreationModificationUsersAndDateRecordPreparationStep; import com.constellio.model.entities.schemas.preparationSteps.ValidateCyclicReferencesRecordPreparationStep; import com.constellio.model.entities.schemas.preparationSteps.ValidateMetadatasRecordPreparationStep; import com.constellio.model.entities.schemas.preparationSteps.ValidateUsingSchemaValidatorsRecordPreparationStep; import com.constellio.model.extensions.ModelLayerCollectionExtensions; import com.constellio.model.extensions.events.records.RecordCreationEvent; import com.constellio.model.extensions.events.records.RecordEvent; import com.constellio.model.extensions.events.records.RecordInCreationBeforeSaveEvent; import com.constellio.model.extensions.events.records.RecordInCreationBeforeValidationAndAutomaticValuesCalculationEvent; import com.constellio.model.extensions.events.records.RecordInModificationBeforeSaveEvent; import com.constellio.model.extensions.events.records.RecordInModificationBeforeValidationAndAutomaticValuesCalculationEvent; import com.constellio.model.extensions.events.records.RecordLogicalDeletionEvent; import com.constellio.model.extensions.events.records.RecordModificationEvent; import com.constellio.model.extensions.events.records.RecordRestorationEvent; import com.constellio.model.extensions.events.records.TransactionExecutionBeforeSaveEvent; import com.constellio.model.frameworks.validation.DecoratedValidationsErrors; import com.constellio.model.frameworks.validation.ValidationErrors; import com.constellio.model.services.contents.ContentManager; import com.constellio.model.services.contents.ContentModifications; import com.constellio.model.services.contents.ContentModificationsBuilder; import com.constellio.model.services.contents.ParsedContentProvider; import com.constellio.model.services.encrypt.EncryptionServices; import com.constellio.model.services.factories.ModelLayerFactory; import com.constellio.model.services.parser.LanguageDetectionManager; import com.constellio.model.services.records.RecordServicesException.UnresolvableOptimisticLockingConflict; import com.constellio.model.services.records.RecordServicesException.ValidationException; import com.constellio.model.services.records.RecordServicesRuntimeException.CannotSetIdsToReindexInEmptyTransaction; import com.constellio.model.services.records.RecordServicesRuntimeException.NewReferenceToOtherLogicallyDeletedRecord; import com.constellio.model.services.records.RecordServicesRuntimeException.RecordServicesRuntimeException_CannotDelayFlushingOfRecordsInCache; import com.constellio.model.services.records.RecordServicesRuntimeException.RecordServicesRuntimeException_ExceptionWhileCalculating; import com.constellio.model.services.records.RecordServicesRuntimeException.RecordServicesRuntimeException_RecordsFlushingFailed; import com.constellio.model.services.records.RecordServicesRuntimeException.RecordServicesRuntimeException_TransactionHasMoreThan100000Records; import com.constellio.model.services.records.RecordServicesRuntimeException.RecordServicesRuntimeException_TransactionWithMoreThan1000RecordsCannotHaveTryMergeOptimisticLockingResolution; import com.constellio.model.services.records.RecordServicesRuntimeException.UnresolvableOptimsiticLockingCausingInfiniteLoops; import com.constellio.model.services.records.cache.RecordsCache; import com.constellio.model.services.records.cache.RecordsCaches; import com.constellio.model.services.records.extractions.RecordPopulateServices; import com.constellio.model.services.records.populators.SearchFieldsPopulator; import com.constellio.model.services.records.populators.SortFieldsPopulator; import com.constellio.model.services.records.preparation.RecordsToReindexResolver; import com.constellio.model.services.schemas.MetadataList; import com.constellio.model.services.schemas.ModificationImpactCalculator; import com.constellio.model.services.schemas.SchemaUtils; import com.constellio.model.services.search.SearchServices; import com.constellio.model.services.search.query.ReturnedMetadatasFilter; import com.constellio.model.services.search.query.logical.LogicalSearchQuery; import com.constellio.model.services.search.query.logical.LogicalSearchQueryOperators; import com.constellio.model.services.search.query.logical.condition.LogicalSearchCondition; import com.constellio.model.services.taxonomies.TaxonomiesManager; import com.constellio.model.utils.DependencyUtilsRuntimeException.CyclicDependency; public class RecordServicesImpl extends BaseRecordServices { private static final Logger LOGGER = LoggerFactory.getLogger(RecordServicesImpl.class); private final RecordDao recordDao; private final RecordDao eventsDao; private final RecordDao notificationsDao; private final ModelLayerFactory modelFactory; private final UniqueIdGenerator uniqueIdGenerator; private final RecordsCaches recordsCaches; public RecordServicesImpl(RecordDao recordDao, RecordDao eventsDao, RecordDao notificationsDao, ModelLayerFactory modelFactory, DataStoreTypesFactory typesFactory, UniqueIdGenerator uniqueIdGenerator, RecordsCaches recordsCaches) { super(modelFactory); this.recordDao = recordDao; this.eventsDao = eventsDao; this.notificationsDao = notificationsDao; this.modelFactory = modelFactory; this.uniqueIdGenerator = uniqueIdGenerator; this.recordsCaches = recordsCaches; } public void executeWithImpactHandler(Transaction transaction, RecordModificationImpactHandler handler) throws RecordServicesException { executeWithImpactHandler(transaction, handler, 0); } public void executeWithoutImpactHandling(Transaction transaction) throws RecordServicesException { executeWithImpactHandler(transaction, new RecordModificationImpactHandler() { @Override public void prepareToHandle(ModificationImpact modificationImpact) { } @Override public void handle() { } @Override public void cancel() { } }); } public void execute(Transaction transaction) throws RecordServicesException { execute(transaction, 0); } public void execute(Transaction transaction, int attempt) throws RecordServicesException { validateNotTooMuchRecords(transaction); if (transaction.getRecords().isEmpty()) { if (!transaction.getIdsToReindex().isEmpty()) { throw new CannotSetIdsToReindexInEmptyTransaction(); } return; } String collection = transaction.getCollection(); MetadataSchemaTypes schemaTypes = modelFactory.getMetadataSchemasManager().getSchemaTypes(collection); try { transaction.sortRecords(schemaTypes); } catch (CyclicDependency e) { LOGGER.info("Cyclic dependency detected, a validation error will be thrown", e); } prepareRecords(transaction); List<ModificationImpact> impacts = getModificationImpacts(transaction, false); if (impacts.isEmpty()) { saveContentsAndRecords(transaction, null, attempt); } else { Transaction newTransaction = new Transaction(transaction); for (ModificationImpact impact : impacts) { LOGGER.debug("Handling modification impact. Reindexing " + impact.getMetadataToReindex() + " for records of " + impact.getLogicalSearchCondition().toString()); LogicalSearchQuery searchQuery = new LogicalSearchQuery(impact.getLogicalSearchCondition()); List<Record> recordsFound = modelFactory.newSearchServices().search(searchQuery); for (Record record : recordsFound) { if (!newTransaction.isContainingUpdatedRecord(record)) { newTransaction.addUpdate(record); } } newTransaction.getRecordUpdateOptions().getTransactionRecordsReindexation() .addReindexedMetadatas(impact.getMetadataToReindex()); } execute(newTransaction); } } public List<BatchProcess> executeHandlingImpactsAsync(Transaction transaction) throws RecordServicesException { if (!transaction.getRecords().isEmpty()) { AddToBatchProcessImpactHandler handler = addToBatchProcessModificationImpactHandler(); executeWithImpactHandler(transaction, handler); return handler.getAllCreatedBatchProcesses(); } else { return Collections.emptyList(); } } public void executeWithImpactHandler(Transaction transaction, RecordModificationImpactHandler handler, int attempt) throws RecordServicesException { validateNotTooMuchRecords(transaction); prepareRecords(transaction); if (handler != null) { List<ModificationImpact> impacts = getModificationImpacts(transaction, true); for (ModificationImpact modificationImpact : impacts) { handler.prepareToHandle(modificationImpact); } } saveContentsAndRecords(transaction, handler, attempt); } void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e1) { throw new RuntimeException(e1); } } void handleOptimisticLocking(TransactionDTO transactionDTO, Transaction transaction, RecordModificationImpactHandler handler, OptimisticLocking e, int attempt) throws RecordServicesException { if (attempt > 35) { throw new UnresolvableOptimsiticLockingCausingInfiniteLoops(transactionDTO); } //Will wait up to 30 seconds given 35 attempt are made long sleepDuration = 50 * attempt; if (sleepDuration > 0) { sleep(sleepDuration); } OptimisticLockingResolution resolution = transaction.getRecordUpdateOptions().getOptimisticLockingResolution(); if (resolution == OptimisticLockingResolution.EXCEPTION || transaction.getModifiedRecords().isEmpty()) { throw new RecordServicesException.OptimisticLocking(transactionDTO, e); } else if (resolution == OptimisticLockingResolution.TRY_MERGE) { mergeRecords(transaction, e.getId()); if (handler == null) { execute(transaction, attempt + 1); } else { executeWithImpactHandler(transaction, handler, attempt + 1); } } } void mergeRecords(Transaction transaction, String failedId) throws RecordServicesException.UnresolvableOptimisticLockingConflict { List<LogicalSearchCondition> conditions = new ArrayList<>(); for (Record record : transaction.getRecords()) { if (record.isSaved()) { conditions.add(LogicalSearchQueryOperators.where(Schemas.IDENTIFIER).isEqualTo(record.getId()) .andWhere(Schemas.VERSION).isNotEqual(record.getVersion())); } } if (conditions.isEmpty()) { for (Record record : transaction.getRecords()) { if (!record.isSaved()) { try { getDocumentById(record.getId()); throw new RecordServicesRuntimeException.IdAlreadyExisting(record.getId()); } catch (RecordServicesRuntimeException.NoSuchRecordWithId e) { //OK } } } } if (conditions.isEmpty()) { throw new RecordServicesException.UnresolvableOptimisticLockingConflict(failedId); } LogicalSearchCondition condition = fromAllSchemasIn(transaction.getCollection()).whereAnyCondition(conditions); List<Record> modifiedRecordVersions = modelFactory.newSearchServices().search(new LogicalSearchQuery(condition)); for (Record newRecordVersion : modifiedRecordVersions) { Record transactionRecord = null; MetadataSchemaTypes types = modelFactory.getMetadataSchemasManager().getSchemaTypes(transaction.getCollection()); MetadataSchema metadataSchema = types.getSchema(newRecordVersion.getSchemaCode()); for (Record aTransactionRecord : transaction.getRecords()) { if (aTransactionRecord.getId().equals(newRecordVersion.getId())) { transactionRecord = aTransactionRecord; break; } } try { ((RecordImpl) transactionRecord).merge((RecordImpl) newRecordVersion, metadataSchema); } catch (RecordRuntimeException.CannotMerge e) { throw new UnresolvableOptimisticLockingConflict(e); } } } public Record toRecord(RecordDTO recordDTO, boolean allFields) { Record record = new RecordImpl(recordDTO, allFields); newAutomaticMetadataServices() .loadTransientEagerMetadatas((RecordImpl) record, newRecordProviderWithoutPreloadedRecords(), new RecordUpdateOptions()); return record; } public List<Record> toRecords(List<RecordDTO> recordDTOs, boolean allFields) { List<Record> records = new ArrayList<Record>(); for (RecordDTO recordDTO : recordDTOs) { records.add(toRecord(recordDTO, allFields)); } return Collections.unmodifiableList(records); } public long documentsCount() { return recordDao.documentsCount(); } @Override public Record getRecordByMetadata(Metadata metadata, String value) { if (!metadata.isUniqueValue()) { throw new IllegalArgumentException("Metadata '" + metadata + "' is not unique"); } if (metadata.getCode().startsWith("global")) { throw new IllegalArgumentException("Metadata '" + metadata + "' is global, which has no specific schema type."); } SearchServices searchServices = modelLayerFactory.newSearchServices(); MetadataSchemaTypes types = modelFactory.getMetadataSchemasManager().getSchemaTypes(metadata.getCollection()); String schemaTypeCode = new SchemaUtils().getSchemaTypeCode(metadata); MetadataSchemaType schemaType = types.getSchemaType(schemaTypeCode); LogicalSearchCondition condition = from(schemaType).where(metadata).isEqualTo(value); return searchServices.searchSingleResult(condition); } public Record getDocumentById(String id) { try { Record record = new RecordImpl(recordDao.get(id), true); newAutomaticMetadataServices() .loadTransientEagerMetadatas((RecordImpl) record, newRecordProviderWithoutPreloadedRecords(), new RecordUpdateOptions()); recordsCaches.insert(record); return record; } catch (NoSuchRecordWithId e) { throw new RecordServicesRuntimeException.NoSuchRecordWithId(id, e); } } public List<Record> getRecordsById(String collection, List<String> ids) { LogicalSearchQuery query = new LogicalSearchQuery(fromAllSchemasIn(collection).where(Schemas.IDENTIFIER).isIn(ids)); return modelFactory.newSearchServices().search(query); } void prepareRecords(Transaction transaction) throws RecordServicesException.ValidationException { prepareRecords(transaction, null); } void prepareRecords(Transaction transaction, String onlyValidateRecord) throws RecordServicesException.ValidationException { RecordPopulateServices recordPopulateServices = modelLayerFactory.newRecordPopulateServices(); RecordProvider recordProvider = newRecordProvider(null, transaction); RecordValidationServices validationServices = newRecordValidationServices(recordProvider); RecordAutomaticMetadataServices automaticMetadataServices = newAutomaticMetadataServices(); TransactionRecordsReindexation reindexation = transaction.getRecordUpdateOptions().getTransactionRecordsReindexation(); MetadataSchemaTypes types = modelFactory.getMetadataSchemasManager().getSchemaTypes(transaction.getCollection()); RecordUpdateOptions options = transaction.getRecordUpdateOptions(); if (transaction.getRecordUpdateOptions().getRecordsFlushing() != RecordsFlushing.NOW()) { RecordsCache cache = recordsCaches.getCache(transaction.getCollection()); for (Record record : transaction.getRecords()) { if (record.isDirty() && record.isSaved() && cache.getCacheConfigOf(record.getSchemaCode()) != null) { throw new RecordServicesRuntimeException_CannotDelayFlushingOfRecordsInCache(record.getSchemaCode(), record.getId()); } } } for (Record record : transaction.getRecords()) { MetadataSchemaType schemaType = types.getSchemaType(record.getTypeCode()); if (schemaType.isReadOnlyLocked() && !options.isAllowSchemaTypeLockedRecordsModification()) { throw new RecordServicesRuntimeException.SchemaTypeOfARecordHasReadOnlyLock(record.getTypeCode(), record.getId()); } } ModelLayerCollectionExtensions extensions = modelFactory.getExtensions().forCollection(transaction.getCollection()); for (Record record : transaction.getRecords()) { if (record.isDirty()) { if (record.isSaved()) { MetadataList modifiedMetadatas = record.getModifiedMetadatas(types); extensions.callRecordInModificationBeforeValidationAndAutomaticValuesCalculation( new RecordInModificationBeforeValidationAndAutomaticValuesCalculationEvent(record, modifiedMetadatas), options); } else { extensions.callRecordInCreationBeforeValidationAndAutomaticValuesCalculation( new RecordInCreationBeforeValidationAndAutomaticValuesCalculationEvent( record, transaction.getUser()), options); } } } boolean validations = transaction.getRecordUpdateOptions().isValidationsEnabled(); //List<Record> records = RecordUtils.sortRecordByDependency(types, transaction.getRecords()); //List<Record> records = DependencyUtils.sortRecordByDependency(types, transaction.getRecords()); ParsedContentProvider parsedContentProvider = new ParsedContentProvider(modelFactory.getContentManager(), transaction.getParsedContentCache()); for (Record record : transaction.getRecords()) { recordPopulateServices.populate(record, parsedContentProvider); MetadataSchema schema = types.getSchema(record.getSchemaCode()); if (onlyValidateRecord == null || onlyValidateRecord.equals(record.getId())) { for (RecordPreparationStep step : schema.getPreparationSteps()) { if (step instanceof CalculateMetadatasRecordPreparationStep) { try { for (Metadata metadata : step.getMetadatas()) { automaticMetadataServices.updateAutomaticMetadata((RecordImpl) record, recordProvider, metadata, reindexation, types, transaction.getRecordUpdateOptions()); } } catch (RuntimeException e) { throw new RecordServicesRuntimeException_ExceptionWhileCalculating(record.getId(), e); } validationServices.validateAccess(record, transaction); } else if (step instanceof UpdateCreationModificationUsersAndDateRecordPreparationStep) { if (transaction.getRecordUpdateOptions().isUpdateModificationInfos()) { updateCreationModificationUsersAndDates(record, transaction, types.getSchema(record.getSchemaCode())); } } else if (step instanceof SequenceRecordPreparationStep) { SequencesManager sequencesManager = modelFactory.getDataLayerFactory().getSequencesManager(); for (Metadata metadata : step.getMetadatas()) { SequenceDataEntry dataEntry = (SequenceDataEntry) metadata.getDataEntry(); if (dataEntry.getFixedSequenceCode() != null) { if (record.get(metadata) == null) { String sequenceCode = dataEntry.getFixedSequenceCode(); String value = format(metadata.getInputMask(), "" + sequencesManager.next(sequenceCode)); record.set(metadata, sequenceCode == null ? null : value); } } else { Metadata metadataProvidingSequenceCode = schema .getMetadata(dataEntry.getMetadataProvidingSequenceCode()); if (record.isModified(metadataProvidingSequenceCode) && !record.isModified(metadata)) { String sequenceCode = record.get(metadataProvidingSequenceCode); String value = sequenceCode == null ? null : format(metadata.getInputMask(), "" + sequencesManager.next(sequenceCode)); record.set(metadata, sequenceCode == null ? null : value); } } } if (validations) { validationServices.validateCyclicReferences(record, recordProvider, types, schema.getMetadatas()); } } else if (step instanceof ValidateCyclicReferencesRecordPreparationStep) { if (validations) { validationServices.validateCyclicReferences(record, recordProvider, types, schema.getMetadatas()); } } else if (step instanceof ValidateMetadatasRecordPreparationStep) { if (validations) { validationServices.validateMetadatas(record, recordProvider, transaction, step.getMetadatas()); } } else if (step instanceof ValidateUsingSchemaValidatorsRecordPreparationStep) { if (validations) { validationServices.validateSchemaUsingCustomSchemaValidator(record, recordProvider, transaction); } } } if (transaction.getRecordUpdateOptions().getTransactionRecordsReindexation().isReindexAll() && schema.hasMetadataWithCode(Schemas.MARKED_FOR_REINDEXING.getLocalCode()) && !record.isModified(Schemas.MARKED_FOR_REINDEXING) && !transaction.getIdsToReindex().contains(record.getId())) { record.set(Schemas.MARKED_FOR_REINDEXING, null); } } } new RecordsToReindexResolver(types).findRecordsToReindex(transaction); ValidationErrors errors = new ValidationErrors(); boolean singleRecordTransaction = transaction.getRecords().size() == 1; boolean catchValidationsErrors = transaction.getRecordUpdateOptions().isCatchExtensionsValidationsErrors(); ValidationErrors transactionExtensionErrors = catchValidationsErrors ? new ValidationErrors() : new DecoratedValidationsErrors(errors); extensions.callTransactionExecutionBeforeSave( new TransactionExecutionBeforeSaveEvent(transaction, transactionExtensionErrors), options); if (catchValidationsErrors && !transactionExtensionErrors.isEmptyErrorAndWarnings()) { LOGGER.warn("Validating errors added by extensions : \n" + $(transactionExtensionErrors)); } for (Record record : transaction.getRecords()) { if (record.isDirty()) { ValidationErrors recordErrors = catchValidationsErrors ? new ValidationErrors() : new DecoratedValidationsErrors(errors); if (record.isSaved()) { MetadataList modifiedMetadatas = record.getModifiedMetadatas(types); extensions.callRecordInModificationBeforeSave( new RecordInModificationBeforeSaveEvent(record, modifiedMetadatas, singleRecordTransaction, recordErrors), options); } else { extensions.callRecordInCreationBeforeSave(new RecordInCreationBeforeSaveEvent( record, transaction.getUser(), singleRecordTransaction, recordErrors), options); } if (catchValidationsErrors && !recordErrors.isEmptyErrorAndWarnings()) { LOGGER.warn("Validating errors added by extensions : \n" + $(recordErrors)); } } } if (!errors.isEmpty()) { throw new RecordServicesException.ValidationException(transaction, errors); } } public void validateRecordInTransaction(Record record, Transaction transaction) throws ValidationException { if (transaction.getRecords().isEmpty()) { validateRecord(record); } else { prepareRecords(transaction, record.getId()); } } public void validateRecord(Record record) throws RecordServicesException.ValidationException { Transaction transaction = new Transaction(record); prepareRecords(transaction); } private void updateCreationModificationUsersAndDates(Record record, Transaction transaction, MetadataSchema metadataSchema) { String type = new SchemaUtils().getSchemaTypeCode(record.getSchemaCode()); boolean userGroupOrCollection = User.SCHEMA_TYPE.equals(type) || Group.SCHEMA_TYPE.equals(type) || Collection.SCHEMA_TYPE.equals(type); LocalDateTime now = TimeProvider.getLocalDateTime(); String currentUserId = transaction.getUser() == null ? null : transaction.getUser().getId(); if (!record.isSaved()) { boolean hasCreatedByMetadata = metadataSchema.hasMetadataWithCode(Schemas.CREATED_BY.getLocalCode()); if (!record.isModified(Schemas.CREATED_BY) && !userGroupOrCollection && hasCreatedByMetadata) { record.set(Schemas.CREATED_BY, currentUserId); } if (!record.isModified(Schemas.CREATED_ON)) { record.set(Schemas.CREATED_ON, now); } boolean hasModifiedByMetadata = metadataSchema.hasMetadataWithCode(Schemas.MODIFIED_BY.getLocalCode()); if (!record.isModified(Schemas.MODIFIED_BY) && !userGroupOrCollection && hasModifiedByMetadata) { record.set(Schemas.MODIFIED_BY, record.get(Schemas.CREATED_BY)); } if (!record.isModified(Schemas.MODIFIED_ON)) { record.set(Schemas.MODIFIED_ON, record.get(Schemas.CREATED_ON)); } } else { if (!record.isModified(Schemas.MODIFIED_BY) && !userGroupOrCollection) { record.set(Schemas.MODIFIED_BY, currentUserId); } if (!record.isModified(Schemas.MODIFIED_ON)) { record.set(Schemas.MODIFIED_ON, now); } } } List<ModificationImpact> getModificationImpacts(Transaction transaction, boolean executedAfterTransaction) { SearchServices searchServices = modelFactory.newSearchServices(); TaxonomiesManager taxonomiesManager = modelFactory.getTaxonomiesManager(); MetadataSchemaTypes metadataSchemaTypes = modelFactory.getMetadataSchemasManager().getSchemaTypes( transaction.getCollection()); return calculateImpactOfModification(transaction, taxonomiesManager, searchServices, metadataSchemaTypes, executedAfterTransaction); } void refreshRecordsAndCaches(String collection, List<Record> records, TransactionResponseDTO transactionResponseDTO, MetadataSchemaTypes types) { List<Record> recordsToInsert = new ArrayList<>(); for (Record record : records) { RecordImpl recordImpl = (RecordImpl) record; Long version = transactionResponseDTO.getNewDocumentVersion(record.getId()); if (version != null) { MetadataSchema schema = types.getSchema(record.getSchemaCode()); recordImpl.markAsSaved(version, schema); recordsToInsert.add(record); } } recordsCaches.insert(collection, recordsToInsert); } void saveContentsAndRecords(Transaction transaction, RecordModificationImpactHandler modificationImpactHandler, int attempt) throws RecordServicesException { ContentManager contentManager = modelFactory.getContentManager(); MetadataSchemaTypes types = modelFactory.getMetadataSchemasManager().getSchemaTypes(transaction.getCollection()); ContentModifications contentModificationsBuilder = findContentsModificationsIn(types, transaction); try { for (String deletedContent : contentModificationsBuilder.getDeletedContentsVersionsHashes()) { contentManager.silentlyMarkForDeletionIfNotReferenced(deletedContent); } saveTransactionDTO(transaction, modificationImpactHandler, attempt); } catch (RecordServicesException | RecordServicesRuntimeException e) { for (String newContent : contentModificationsBuilder.getContentsWithNewVersion()) { contentManager.silentlyMarkForDeletionIfNotReferenced(newContent); } throw e; } } ContentModifications findContentsModificationsIn(MetadataSchemaTypes types, Transaction transaction) { return new ContentModificationsBuilder(types).buildForModifiedRecords(transaction.getRecords()); } void saveTransactionDTO(Transaction transaction, RecordModificationImpactHandler modificationImpactHandler, int attempt) throws RecordServicesException { List<Record> modifiedOrUnsavedRecords = transaction.getModifiedRecords(); if (!modifiedOrUnsavedRecords.isEmpty() || !transaction.getIdsToReindex().isEmpty()) { TransactionDTO transactionDTO = createTransactionDTO(transaction, modifiedOrUnsavedRecords); try { MetadataSchemaTypes metadataSchemaTypes = modelFactory.getMetadataSchemasManager().getSchemaTypes( transaction.getCollection()); List<RecordEvent> recordEvents = prepareRecordEvents(modifiedOrUnsavedRecords, metadataSchemaTypes); TransactionResponseDTO transactionResponseDTO = recordDao.execute(transactionDTO); modelFactory.newLoggingServices().logTransaction(transaction); refreshRecordsAndCaches(transaction.getCollection(), modifiedOrUnsavedRecords, transactionResponseDTO, metadataSchemaTypes); if (modificationImpactHandler != null) { modificationImpactHandler.handle(); } callExtensions(transaction.getCollection(), recordEvents, transaction.getRecordUpdateOptions()); } catch (OptimisticLocking e) { if (modificationImpactHandler != null) { modificationImpactHandler.cancel(); } LOGGER.trace("Optimistic locking, handling with specified resolution {}", transaction.getRecordUpdateOptions() .getOptimisticLockingResolution().name(), e); handleOptimisticLocking(transactionDTO, transaction, modificationImpactHandler, e, attempt); } catch (ReferenceToNonExistentIndex e) { throw new NewReferenceToOtherLogicallyDeletedRecord(e.getId(), e); } } } private List<RecordEvent> prepareRecordEvents(List<Record> modifiedOrUnsavedRecords, MetadataSchemaTypes types) { List<RecordEvent> events = new ArrayList<>(); for (Record record : modifiedOrUnsavedRecords) { if (record.isSaved()) { if (record.isModified(Schemas.LOGICALLY_DELETED_STATUS)) { if (LangUtils.isFalseOrNull(record.get(Schemas.LOGICALLY_DELETED_STATUS))) { events.add(new RecordRestorationEvent(record)); } else { events.add(new RecordLogicalDeletionEvent(record)); } } else { MetadataList modifiedMetadatas = record.getModifiedMetadatas(types); events.add( new RecordModificationEvent(record, modifiedMetadatas)); } } else { events.add(new RecordCreationEvent(record)); } } return events; } private void callExtensions(String collection, List<RecordEvent> recordEvents, RecordUpdateOptions options) { ModelLayerCollectionExtensions extensions = modelFactory.getExtensions().forCollection(collection); for (RecordEvent recordEvent : recordEvents) { if (recordEvent instanceof RecordCreationEvent) { extensions.callRecordCreated((RecordCreationEvent) recordEvent, options); } else if (recordEvent instanceof RecordModificationEvent) { extensions.callRecordModified((RecordModificationEvent) recordEvent, options); } else if (recordEvent instanceof RecordLogicalDeletionEvent) { extensions.callRecordLogicallyDeleted((RecordLogicalDeletionEvent) recordEvent); } else if (recordEvent instanceof RecordRestorationEvent) { extensions.callRecordRestored((RecordRestorationEvent) recordEvent); } } } TransactionDTO createTransactionDTO(Transaction transaction, List<Record> modifiedOrUnsavedRecords) { String collection = transaction.getCollection(); List<RecordDTO> addedRecords = new ArrayList<>(); List<RecordDeltaDTO> modifiedRecordDTOs = new ArrayList<>(); LanguageDetectionManager languageDetectionManager = modelFactory.getLanguageDetectionManager(); ContentManager contentManager = modelFactory.getContentManager(); List<String> collectionLanguages = modelFactory.getCollectionsListManager().getCollectionLanguages(collection); List<FieldsPopulator> fieldsPopulators = new ArrayList<>(); MetadataSchemaTypes types = modelFactory.getMetadataSchemasManager().getSchemaTypes(collection); ParsedContentProvider parsedContentProvider = new ParsedContentProvider(contentManager, transaction.getParsedContentCache()); fieldsPopulators.add(new SearchFieldsPopulator( types, transaction.getRecordUpdateOptions().isFullRewrite(), parsedContentProvider, collectionLanguages)); //fieldsPopulators.add(new AutocompleteFieldPopulator()); fieldsPopulators.add(new SortFieldsPopulator(types, transaction.getRecordUpdateOptions().isFullRewrite())); Factory<EncryptionServices> encryptionServicesFactory = new Factory<EncryptionServices>() { @Override public EncryptionServices get() { return modelLayerFactory.newEncryptionServices(); } }; List<String> ids = transaction.getRecordIds(); Set<String> markedForReindexing = new HashSet<>(); for (String id : transaction.getIdsToReindex()) { if (!ids.contains(id)) { markedForReindexing.add(id); } else { transaction.getRecord(id).set(Schemas.MARKED_FOR_REINDEXING, true); } } for (Record record : modifiedOrUnsavedRecords) { MetadataSchema schema = modelFactory.getMetadataSchemasManager().getSchemaTypes(collection) .getSchema(record.getSchemaCode()); if (!record.isSaved()) { addedRecords.add(((RecordImpl) record).toNewDocumentDTO(schema, fieldsPopulators)); } else { RecordImpl recordImpl = (RecordImpl) record; if (recordImpl.isDirty() && !transaction.getRecordUpdateOptions().isFullRewrite()) { modifiedRecordDTOs.add(recordImpl.toRecordDeltaDTO(schema, fieldsPopulators)); } else if (transaction.getRecordUpdateOptions().isFullRewrite()) { addedRecords.add(((RecordImpl) record).toDocumentDTO(schema, fieldsPopulators)); } } } return new TransactionDTO( transaction.getId(), transaction.getRecordUpdateOptions().getRecordsFlushing(), addedRecords, modifiedRecordDTOs) .withSkippingReferenceToLogicallyDeletedValidation( transaction.isSkippingReferenceToLogicallyDeletedValidation()) .withFullRewrite(transaction.getRecordUpdateOptions().isFullRewrite()) .withMarkedForReindexing(markedForReindexing); } public Record newRecordWithSchema(MetadataSchema schema, String id) { Record record = new RecordImpl(schema.getCode(), schema.getCollection(), id); for (Metadata metadata : schema.getMetadatas().onlyWithDefaultValue().onlyManuals()) { if (metadata.isMultivalue()) { List<Object> values = new ArrayList<>(); values.addAll((List) metadata.getDefaultValue()); record.set(metadata, values); } else { record.set(metadata, metadata.getDefaultValue()); } } return record; } public Record newRecordWithSchema(MetadataSchema schema) { String id; if ("collection_default".equals(schema.getCode())) { id = schema.getCollection(); } else if (!schema.isInTransactionLog()) { id = uniqueIdGenerator.next() + "ZZ"; } else { id = uniqueIdGenerator.next(); } return newRecordWithSchema(schema, id); } public RecordAutomaticMetadataServices newAutomaticMetadataServices() { return new RecordAutomaticMetadataServices(modelFactory.getMetadataSchemasManager(), modelFactory.getTaxonomiesManager(), modelFactory.getSystemConfigurationsManager(), modelFactory.getModelLayerLogger(), modelFactory.newSearchServices()); } public RecordValidationServices newRecordValidationServices(RecordProvider recordProvider) { return new RecordValidationServices(newConfigProvider(), recordProvider, modelFactory.getMetadataSchemasManager(), modelFactory.newSearchServices(), modelFactory.newAuthorizationsServices()); } public ConfigProvider newConfigProvider() { return new ConfigProvider() { @Override public <T> T get(SystemConfiguration config) { return modelFactory.getSystemConfigurationsManager().getValue(config); } }; } public RecordDeleteServices newRecordDeleteServices() { return new RecordDeleteServices(recordDao, modelFactory); } public AddToBatchProcessImpactHandler addToBatchProcessModificationImpactHandler() { return new AddToBatchProcessImpactHandler(modelFactory.getBatchProcessesManager(), modelFactory.newSearchServices()); } public void refresh(List<?> records) { for (Object item : records) { Record record; if (item instanceof Record) { record = (Record) item; } else { record = ((RecordWrapper) item).getWrappedRecord(); } if (record != null && record.isSaved()) { try { RecordDTO recordDTO = recordDao.get(record.getId()); ((RecordImpl) record).refresh(recordDTO.getVersion(), recordDTO); } catch (NoSuchRecordWithId noSuchRecordWithId) { LOGGER.debug("Deleted record is disconnected"); ((RecordImpl) record).markAsDisconnected(); } } } } RecordProvider newRecordProvider(RecordProvider nestedProvider, Transaction transaction) { return new RecordProvider(modelLayerFactory.newRecordServices(), nestedProvider, transaction.getRecords(), transaction); } RecordProvider newRecordProviderWithoutPreloadedRecords() { return new RecordProvider(modelLayerFactory.newRecordServices(), null, null, null); } public final List<String> getRecordTitles(String collection, List<String> recordIds) { List<String> recordTitles = new ArrayList<>(); if (recordIds.isEmpty()) { return new ArrayList<>(); } LogicalSearchCondition condition = fromAllSchemasIn(collection).where(Schemas.IDENTIFIER).isIn(recordIds); LogicalSearchQuery query = new LogicalSearchQuery(condition) .setReturnedMetadatas(ReturnedMetadatasFilter.idVersionSchemaTitle()); List<Record> records = modelFactory.newSearchServices().search(query); for (Record record : records) { recordTitles.add((String) record.get(Schemas.TITLE)); } return recordTitles; } RecordUtils newRecordUtils() { return new RecordUtils(); } public List<ModificationImpact> calculateImpactOfModification(Transaction transaction, TaxonomiesManager taxonomiesManager, SearchServices searchServices, MetadataSchemaTypes metadataSchemaTypes, boolean executedAfterTransaction) { if (transaction.getRecords().isEmpty()) { return new ArrayList<>(); } ModificationImpactCalculator calculator = newModificationImpactCalculator(taxonomiesManager, metadataSchemaTypes, searchServices); return calculator.findTransactionImpact(transaction, executedAfterTransaction); } @Override public RecordsCaches getRecordsCaches() { return recordsCaches; } public ModificationImpactCalculator newModificationImpactCalculator(TaxonomiesManager taxonomiesManager, MetadataSchemaTypes metadataSchemaTypes, SearchServices searchServices) { List<Taxonomy> taxonomies = taxonomiesManager.getEnabledTaxonomies(metadataSchemaTypes.getCollection()); return new ModificationImpactCalculator(metadataSchemaTypes, taxonomies, searchServices, this); } public boolean isRestorable(Record record, User user) { refresh(record); refresh(user); return newRecordDeleteServices().isRestorable(record, user); } public void restore(Record record, User user) { refresh(record); refresh(user); newRecordDeleteServices().restore(record, user); } public boolean isPhysicallyDeletable(Record record, User user) { refresh(record); refresh(user); return newRecordDeleteServices().isPhysicallyDeletable(record, user); } public boolean isPhysicallyDeletable(Record record, User user, RecordPhysicalDeleteOptions options) { refresh(record); refresh(user); return newRecordDeleteServices().isPhysicallyDeletable(record, user, options); } public void physicallyDelete(Record record, User user) { refresh(record); refresh(user); newRecordDeleteServices().physicallyDelete(record, user); } public void physicallyDeleteNoMatterTheStatus(Record record, User user, RecordPhysicalDeleteOptions options) { refresh(record); refresh(user); newRecordDeleteServices().physicallyDeleteNoMatterTheStatus(record, user, options); } public void physicallyDelete(Record record, User user, RecordPhysicalDeleteOptions options) { refresh(record); refresh(user); newRecordDeleteServices().physicallyDelete(record, user, options); } public boolean isLogicallyDeletable(Record record, User user) { refresh(record); refresh(user); return newRecordDeleteServices().isLogicallyDeletable(record, user); } public boolean isLogicallyThenPhysicallyDeletable(Record record, User user) { refresh(record); refresh(user); return newRecordDeleteServices().isLogicallyThenPhysicallyDeletable(record, user); } public boolean isLogicallyThenPhysicallyDeletable(Record record, User user, RecordPhysicalDeleteOptions options) { refresh(record); refresh(user); return newRecordDeleteServices().isLogicallyThenPhysicallyDeletable(record, user, options); } public boolean isPrincipalConceptLogicallyDeletableExcludingContent(Record record, User user) { refresh(record); refresh(user); return newRecordDeleteServices().isPrincipalConceptLogicallyDeletableExcludingContent(record, user); } public boolean isPrincipalConceptLogicallyDeletableIncludingContent(Record record, User user) { refresh(record); refresh(user); return newRecordDeleteServices().isPrincipalConceptLogicallyDeletableIncludingContent(record, user); } public void logicallyDelete(Record record, User user) { logicallyDelete(record, user, new RecordLogicalDeleteOptions()); } public void logicallyDelete(Record record, User user, RecordLogicalDeleteOptions options) { refresh(record); refresh(user); // String recordSchemaType = new SchemaUtils().getSchemaTypeCode(record.getSchemaCode()); // if (taxonomy != null && taxonomy.getSchemaTypes().contains(recordSchemaType)) { // if (options.behaviorForRecordsAttachedToTaxonomy == LogicallyDeleteTaxonomyRecordsBehavior.KEEP_RECORDS) { // newRecordDeleteServices().logicallyDeletePrincipalConceptExcludingRecords(record, user); // } else { // newRecordDeleteServices().logicallyDeletePrincipalConceptIncludingRecords(record, user); // } // } else { newRecordDeleteServices().logicallyDelete(record, user, options); // } refresh(record); } public List<Record> getVisibleRecordsWithReferenceTo(Record record, User user) { refresh(record); refresh(user); return newRecordDeleteServices().getVisibleRecordsWithReferenceToRecordInHierarchy(record, user); } public boolean isReferencedByOtherRecords(Record record) { return newRecordDeleteServices().isReferencedByOtherRecords(record); } private void validateNotTooMuchRecords(Transaction transaction) { if (transaction.getRecords().size() > 100000) { throw new RecordServicesRuntimeException_TransactionHasMoreThan100000Records(transaction.getRecords().size()); } else if (transaction.getRecords().size() > 1000) { RecordUpdateOptions recordUpdateOptions = transaction.getRecordUpdateOptions(); if (recordUpdateOptions.getOptimisticLockingResolution() == OptimisticLockingResolution.TRY_MERGE) { throw new RecordServicesRuntimeException_TransactionWithMoreThan1000RecordsCannotHaveTryMergeOptimisticLockingResolution(); } } } public void flush() { try { recordDao.flush(); eventsDao.flush(); notificationsDao.flush(); } catch (RecordDaoRuntimeException_RecordsFlushingFailed e) { throw new RecordServicesRuntimeException_RecordsFlushingFailed(e); } } public void removeOldLocks() { recordDao.removeOldLocks(); } public void recalculate(RecordWrapper recordWrapper) { recalculate(recordWrapper.getWrappedRecord()); } public void recalculate(Record record) { newAutomaticMetadataServices().updateAutomaticMetadatas( (RecordImpl) record, newRecordProviderWithoutPreloadedRecords(), TransactionRecordsReindexation.ALL(), new RecordUpdateOptions()); } @Override public void loadLazyTransientMetadatas(Record record) { newAutomaticMetadataServices() .loadTransientLazyMetadatas((RecordImpl) record, newRecordProviderWithoutPreloadedRecords(), new RecordUpdateOptions()); } @Override public void reloadEagerTransientMetadatas(Record record) { newAutomaticMetadataServices() .loadTransientEagerMetadatas((RecordImpl) record, newRecordProviderWithoutPreloadedRecords(), new RecordUpdateOptions()); } }