Java tutorial
/* * Copyright 2011-2017 B2i Healthcare Pte Ltd, http://b2i.sg * * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 * * 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 com.b2international.snowowl.snomed.datastore.request; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import org.eclipse.emf.ecore.EObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.b2international.commons.CompareUtils; import com.b2international.snowowl.core.domain.TransactionContext; import com.b2international.snowowl.core.events.Request; import com.b2international.snowowl.core.exceptions.BadRequestException; import com.b2international.snowowl.core.exceptions.ComponentNotFoundException; import com.b2international.snowowl.core.exceptions.ComponentStatusConflictException; import com.b2international.snowowl.core.terminology.ComponentCategory; import com.b2international.snowowl.eventbus.IEventBus; import com.b2international.snowowl.snomed.Component; import com.b2international.snowowl.snomed.Concept; import com.b2international.snowowl.snomed.Description; import com.b2international.snowowl.snomed.SnomedConstants.Concepts; import com.b2international.snowowl.snomed.core.domain.Acceptability; import com.b2international.snowowl.snomed.core.domain.AssociationType; import com.b2international.snowowl.snomed.core.domain.DefinitionStatus; import com.b2international.snowowl.snomed.core.domain.DescriptionInactivationIndicator; import com.b2international.snowowl.snomed.core.domain.InactivationIndicator; import com.b2international.snowowl.snomed.core.domain.SnomedComponent; import com.b2international.snowowl.snomed.core.domain.SnomedConcept; import com.b2international.snowowl.snomed.core.domain.SnomedDescription; import com.b2international.snowowl.snomed.core.domain.SnomedRelationship; import com.b2international.snowowl.snomed.core.domain.SubclassDefinitionStatus; import com.b2international.snowowl.snomed.core.domain.refset.SnomedReferenceSetMember; import com.b2international.snowowl.snomed.core.domain.refset.SnomedReferenceSetMembers; import com.b2international.snowowl.snomed.datastore.SnomedDatastoreActivator; import com.b2international.snowowl.snomed.datastore.SnomedEditingContext; import com.b2international.snowowl.snomed.datastore.SnomedInactivationPlan; import com.b2international.snowowl.snomed.datastore.SnomedInactivationPlan.InactivationReason; import com.google.common.base.Strings; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; /** * @since 4.5 */ public final class SnomedConceptUpdateRequest extends SnomedComponentUpdateRequest { private static final Logger LOGGER = LoggerFactory.getLogger(SnomedConceptUpdateRequest.class); private static final Set<String> FILTERED_REFSET_IDS = ImmutableSet.of( Concepts.REFSET_CONCEPT_INACTIVITY_INDICATOR, Concepts.REFSET_ALTERNATIVE_ASSOCIATION, Concepts.REFSET_MOVED_FROM_ASSOCIATION, Concepts.REFSET_MOVED_TO_ASSOCIATION, Concepts.REFSET_POSSIBLY_EQUIVALENT_TO_ASSOCIATION, Concepts.REFSET_REFERS_TO_ASSOCIATION, Concepts.REFSET_REPLACED_BY_ASSOCIATION, Concepts.REFSET_SAME_AS_ASSOCIATION, Concepts.REFSET_SIMILAR_TO_ASSOCIATION, Concepts.REFSET_WAS_A_ASSOCIATION); private DefinitionStatus definitionStatus; private SubclassDefinitionStatus subclassDefinitionStatus; private InactivationIndicator inactivationIndicator; private Multimap<AssociationType, String> associationTargets; private List<SnomedDescription> descriptions; private List<SnomedRelationship> relationships; private List<SnomedReferenceSetMember> members; SnomedConceptUpdateRequest(String componentId) { super(componentId); } void setDefinitionStatus(DefinitionStatus definitionStatus) { this.definitionStatus = definitionStatus; } void setSubclassDefinitionStatus(SubclassDefinitionStatus subclassDefinitionStatus) { this.subclassDefinitionStatus = subclassDefinitionStatus; } void setInactivationIndicator(InactivationIndicator inactivationIndicator) { this.inactivationIndicator = inactivationIndicator; } void setAssociationTargets(Multimap<AssociationType, String> associationTargets) { this.associationTargets = associationTargets; } void setDescriptions(List<SnomedDescription> descriptions) { this.descriptions = descriptions; } void setRelationships(List<SnomedRelationship> relationships) { this.relationships = relationships; } void setMembers(List<SnomedReferenceSetMember> members) { this.members = members; } @Override public Boolean execute(TransactionContext context) { final Concept concept = context.lookup(getComponentId(), Concept.class); boolean changed = false; changed |= updateModule(context, concept); changed |= updateDefinitionStatus(context, concept); changed |= updateSubclassDefinitionStatus(context, concept); if (descriptions != null) { updateComponents(context, concept, getComponentIds(concept.getDescriptions()), descriptions, id -> SnomedRequests.prepareDeleteDescription(id).build()); } if (relationships != null) { updateComponents(context, concept, getComponentIds(concept.getOutboundRelationships()), relationships, id -> SnomedRequests.prepareDeleteRelationship(id).build()); } if (members != null) { updateComponents(context, concept, getPreviousMemberIds(concept, context), members, id -> SnomedRequests.prepareDeleteMember(id).build()); } changed |= processInactivation(context, concept); if (changed) { if (concept.isSetEffectiveTime()) { concept.unsetEffectiveTime(); } else { if (concept.isReleased()) { long start = new Date().getTime(); final String branchPath = getLatestReleaseBranch(context); if (!Strings.isNullOrEmpty(branchPath)) { final SnomedConcept releasedConcept = SnomedRequests.prepareGetConcept(getComponentId()) .build(SnomedDatastoreActivator.REPOSITORY_UUID, branchPath) .execute(context.service(IEventBus.class)).getSync(); if (releasedConcept == null) { throw new ComponentNotFoundException(ComponentCategory.CONCEPT, getComponentId()); } else if (!isDifferentToPreviousRelease(concept, releasedConcept)) { concept.setEffectiveTime(releasedConcept.getEffectiveTime()); } LOGGER.trace("Previous version comparison took {}", new Date().getTime() - start); } } } } return changed; } private Set<String> getComponentIds(Iterable<? extends Component> components) { return FluentIterable.from(components).transform(c -> c.getId()).toSet(); } private Set<String> getPreviousMemberIds(Concept concept, TransactionContext context) { SnomedReferenceSetMembers members = SnomedRequests.prepareSearchMember() .filterByReferencedComponent(concept.getId()).build().execute(context); return FluentIterable.from(members).filter(m -> !FILTERED_REFSET_IDS.contains(m.getReferenceSetId())) .transform(m -> m.getId()).toSet(); } private boolean isDifferentToPreviousRelease(Concept concept, SnomedConcept releasedConcept) { if (releasedConcept.isActive() != concept.isActive()) return true; if (!releasedConcept.getModuleId().equals(concept.getModule().getId())) return true; if (!releasedConcept.getDefinitionStatus().getConceptId().equals(concept.getDefinitionStatus().getId())) return true; return false; } private boolean updateDefinitionStatus(final TransactionContext context, final Concept concept) { if (null == definitionStatus) { return false; } final String existingDefinitionStatusId = concept.getDefinitionStatus().getId(); final String newDefinitionStatusId = definitionStatus.getConceptId(); if (!existingDefinitionStatusId.equals(newDefinitionStatusId)) { concept.setDefinitionStatus(context.lookup(newDefinitionStatusId, Concept.class)); return true; } else { return false; } } private boolean updateSubclassDefinitionStatus(final TransactionContext context, final Concept concept) { if (null == subclassDefinitionStatus) { return false; } final boolean currentExhaustive = concept.isExhaustive(); final boolean newExhaustive = subclassDefinitionStatus.isExhaustive(); if (currentExhaustive != newExhaustive) { concept.setExhaustive(newExhaustive); return true; } else { return false; } } private boolean processInactivation(final TransactionContext context, final Concept concept) { if (null == isActive() && null == inactivationIndicator && null == associationTargets) { return false; } final boolean currentStatus = concept.isActive(); final boolean newStatus = isActive() == null ? currentStatus : isActive(); final InactivationIndicator newIndicator = inactivationIndicator == null ? InactivationIndicator.RETIRED : inactivationIndicator; final Multimap<AssociationType, String> newAssociationTargets = associationTargets == null ? ImmutableMultimap.<AssociationType, String>of() : associationTargets; if (currentStatus && !newStatus) { // Active --> Inactive: concept inactivation, update indicator and association targets // (using default values if not given) inactivateConcept(context, concept); updateInactivationIndicator(context, newIndicator); updateAssociationTargets(context, newAssociationTargets); return true; } else if (!currentStatus && newStatus) { // Inactive --> Active: concept reactivation, clear indicator and association targets // (using default values at all times) if (inactivationIndicator != null) { throw new BadRequestException( "Cannot reactivate concept and retain or change its inactivation indicator at the same time."); } if (associationTargets != null) { throw new BadRequestException( "Cannot reactivate concept and retain or change its historical association targets at the same time."); } reactivateConcept(context, concept); updateInactivationIndicator(context, newIndicator); updateAssociationTargets(context, newAssociationTargets); return true; } else if (currentStatus == newStatus) { // Same status, allow indicator and/or association targets to be updated if required // (using original values that can be null) updateInactivationIndicator(context, inactivationIndicator); updateAssociationTargets(context, associationTargets); return false; } else { return false; } } private void updateAssociationTargets(final TransactionContext context, Multimap<AssociationType, String> associationTargets) { if (associationTargets == null) { return; } SnomedAssociationTargetUpdateRequest<Concept> associationUpdateRequest = new SnomedAssociationTargetUpdateRequest<>( getComponentId(), Concept.class); associationUpdateRequest.setNewAssociationTargets(associationTargets); associationUpdateRequest.execute(context); } private void updateInactivationIndicator(final TransactionContext context, final InactivationIndicator indicator) { if (indicator == null) { return; } final SnomedInactivationReasonUpdateRequest<Concept> inactivationUpdateRequest = new SnomedInactivationReasonUpdateRequest<>( getComponentId(), Concept.class, Concepts.REFSET_CONCEPT_INACTIVITY_INDICATOR); inactivationUpdateRequest.setInactivationValueId(indicator.getConceptId()); inactivationUpdateRequest.execute(context); } private void inactivateConcept(final TransactionContext context, final Concept concept) { if (!concept.isActive()) { throw new ComponentStatusConflictException(concept.getId(), concept.isActive()); } // Run the basic inactivation plan without setting the inactivation reason or a historical association target; those will be handled separately final SnomedEditingContext editingContext = context.service(SnomedEditingContext.class); final SnomedInactivationPlan inactivationPlan = editingContext.inactivateConcept(concept); inactivationPlan.performInactivation(InactivationReason.RETIRED, null); // The inactivation plan places new inactivation reason members on descriptions, even if one is already present. Fix this by running the update on the descriptions again. for (final Description description : concept.getDescriptions()) { // Add "Concept non-current" reason to active descriptions if (description.isActive()) { SnomedInactivationReasonUpdateRequest<Description> descriptionUpdateRequest = new SnomedInactivationReasonUpdateRequest<>( description.getId(), Description.class, Concepts.REFSET_DESCRIPTION_INACTIVITY_INDICATOR); // XXX: The only other inactivation reason an active description can have is "Pending move"; not sure what the implications are descriptionUpdateRequest.setInactivationValueId( DescriptionInactivationIndicator.CONCEPT_NON_CURRENT.getConceptId()); descriptionUpdateRequest.execute(context); } } } private void reactivateConcept(final TransactionContext context, final Concept concept) { if (concept.isActive()) { throw new ComponentStatusConflictException(concept.getId(), concept.isActive()); } concept.setActive(true); for (final Description description : concept.getDescriptions()) { // Remove "Concept non-current" reason from active descriptions by changing to "no reason given" if (description.isActive()) { SnomedInactivationReasonUpdateRequest<Description> descriptionUpdateRequest = new SnomedInactivationReasonUpdateRequest<>( description.getId(), Description.class, Concepts.REFSET_DESCRIPTION_INACTIVITY_INDICATOR); descriptionUpdateRequest .setInactivationValueId(DescriptionInactivationIndicator.RETIRED.getConceptId()); descriptionUpdateRequest.execute(context); } } } private <T extends EObject, U extends SnomedComponent> boolean updateComponents( final TransactionContext context, final Concept concept, final Set<String> previousComponentIds, final Iterable<U> currentComponents, final Function<String, Request<TransactionContext, ?>> toDeleteRequest) { // pre process all incoming components currentComponents.forEach(component -> { // all incoming components should define their ID in order to be processed if (Strings.isNullOrEmpty(component.getId())) { throw new BadRequestException("New components require their id to be set."); } // all components should have their module ID set if (Strings.isNullOrEmpty(component.getModuleId())) { throw new BadRequestException("It is required to specify the moduleId for the components."); } }); // collect new/changed/deleted components and process them final Map<String, U> currentComponentsById = Maps.uniqueIndex(currentComponents, component -> component.getId()); return Sets.union(previousComponentIds, currentComponentsById.keySet()).stream().map(componentId -> { if (!previousComponentIds.contains(componentId) && currentComponentsById.containsKey(componentId)) { // new component return currentComponentsById.get(componentId).toCreateRequest(concept.getId()); } else if (previousComponentIds.contains(componentId) && currentComponentsById.containsKey(componentId)) { // changed component return currentComponentsById.get(componentId).toUpdateRequest(); } else if (previousComponentIds.contains(componentId) && !currentComponentsById.containsKey(componentId)) { // deleted component return toDeleteRequest.apply(componentId); } else { throw new IllegalStateException("Invalid case, should not happen"); } }).map(req -> req.execute(context)).filter(Boolean.class::isInstance).map(Boolean.class::cast) .reduce(Boolean.FALSE, (r1, r2) -> r1 || r2); } @Override public Set<String> getRequiredComponentIds(TransactionContext context) { final Builder<String> ids = ImmutableSet.<String>builder(); ids.add(getComponentId()); if (getModuleId() != null) { ids.add(getModuleId()); } if (definitionStatus != null) { ids.add(definitionStatus.getConceptId()); } if (inactivationIndicator != null) { ids.add(inactivationIndicator.getConceptId()); } if (associationTargets != null && !associationTargets.isEmpty()) { associationTargets.entries().forEach(entry -> { ids.add(entry.getKey().getConceptId()); ids.add(entry.getValue()); }); } if (!CompareUtils.isEmpty(descriptions)) { descriptions.forEach(description -> { ids.add(description.getModuleId()); ids.add(description.getTypeId()); ids.addAll(description.getAcceptabilityMap().keySet()); ids.addAll(description.getAcceptabilityMap().values().stream().map(Acceptability::getConceptId) .collect(Collectors.toSet())); ids.add(description.getCaseSignificance().getConceptId()); }); } if (!CompareUtils.isEmpty(relationships)) { relationships.forEach(relationship -> { ids.add(relationship.getModuleId()); ids.add(relationship.getTypeId()); ids.add(relationship.getDestinationId()); ids.add(relationship.getCharacteristicType().getConceptId()); ids.add(relationship.getModifier().getConceptId()); }); } if (!CompareUtils.isEmpty(members)) { members.forEach(member -> { ids.add(member.getModuleId()); ids.add(member.getReferenceSetId()); // TODO add specific props? }); } return ids.build(); } }