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.id.memory; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Sets.newHashSet; import static com.google.common.collect.Sets.newLinkedHashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.eclipse.emf.cdo.spi.server.Store; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.b2international.commons.CompareUtils; import com.b2international.commons.VerhoeffCheck; import com.b2international.index.Hits; import com.b2international.index.Index; import com.b2international.index.mapping.DocumentMapping; import com.b2international.index.query.Expressions; import com.b2international.index.query.Query; import com.b2international.snowowl.core.IDisposableService; import com.b2international.snowowl.core.exceptions.BadRequestException; import com.b2international.snowowl.core.terminology.ComponentCategory; import com.b2international.snowowl.snomed.datastore.config.SnomedIdentifierConfiguration; import com.b2international.snowowl.snomed.datastore.id.AbstractSnomedIdentifierService; import com.b2international.snowowl.snomed.datastore.id.SnomedIdentifiers; import com.b2international.snowowl.snomed.datastore.id.domain.IdentifierStatus; import com.b2international.snowowl.snomed.datastore.id.domain.SctId; import com.b2international.snowowl.snomed.datastore.id.gen.ItemIdGenerationStrategy; import com.b2international.snowowl.snomed.datastore.id.reservations.ISnomedIdentifierReservationService; import com.b2international.snowowl.snomed.datastore.internal.id.reservations.SnomedIdentifierReservationServiceImpl; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Provider; /** * {@link Store} based implementation of the identifier service. * * @since 4.5 */ public class DefaultSnomedIdentifierService extends AbstractSnomedIdentifierService implements IDisposableService { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultSnomedIdentifierService.class); private final Index store; private final ItemIdGenerationStrategy generationStrategy; private final AtomicBoolean disposed = new AtomicBoolean(false); /* * Tests only */ DefaultSnomedIdentifierService(final Provider<Index> store, final ItemIdGenerationStrategy generationStrategy) { this(store.get(), generationStrategy, new SnomedIdentifierReservationServiceImpl(), new SnomedIdentifierConfiguration()); } public DefaultSnomedIdentifierService(final Index store, final ItemIdGenerationStrategy generationStrategy, final ISnomedIdentifierReservationService reservationService, final SnomedIdentifierConfiguration config) { super(reservationService, config); this.store = store; this.generationStrategy = generationStrategy; } @Override public Set<String> generate(final String namespace, final ComponentCategory category, final int quantity) { checkNotNull(category, "Component category must not be null."); checkArgument(quantity > 0, "Number of requested IDs should be non-negative."); checkCategory(category); LOGGER.debug("Generating {} component IDs for category {}.", quantity, category.getDisplayName()); final Set<String> componentIds = generateIds(namespace, category, quantity); final Map<String, SctId> sctIds = FluentIterable.from(componentIds) .toMap(componentId -> buildSctId(componentId, IdentifierStatus.ASSIGNED)); putSctIds(sctIds); return componentIds; } @Override public void register(final Set<String> componentIds) { if (CompareUtils.isEmpty(componentIds)) { return; } LOGGER.debug(String.format("Registering {} component IDs.", componentIds.size())); final Map<String, SctId> sctIds = getSctIds(componentIds); final Map<String, SctId> problemSctIds = ImmutableMap.copyOf(Maps.filterValues(sctIds, Predicates.<SctId>not(Predicates.or(SctId::isAvailable, SctId::isReserved, SctId::isAssigned)))); if (!problemSctIds.isEmpty()) { throw new SctIdStatusException( "Cannot register %s component IDs because they are not available, reserved, or already assigned.", problemSctIds); } final Map<String, SctId> availableOrReservedSctIds = ImmutableMap .copyOf(Maps.filterValues(sctIds, Predicates.or(SctId::isAvailable, SctId::isReserved))); for (final SctId sctId : availableOrReservedSctIds.values()) { sctId.setStatus(IdentifierStatus.ASSIGNED.getSerializedName()); } putSctIds(availableOrReservedSctIds); } @Override public Set<String> reserve(final String namespace, final ComponentCategory category, final int quantity) { checkNotNull(category, "Component category must not be null."); checkArgument(quantity > 0, "Number of requested IDs should be greater than zero."); checkCategory(category); LOGGER.debug("Reserving {} component IDs for category {}.", quantity, category.getDisplayName()); final Set<String> componentIds = generateIds(namespace, category, quantity); final Map<String, SctId> sctIds = FluentIterable.from(componentIds) .toMap(componentId -> buildSctId(componentId, IdentifierStatus.RESERVED)); putSctIds(sctIds); return componentIds; } @Override public void release(final Set<String> componentIds) { LOGGER.debug("Releasing {} component IDs.", componentIds.size()); final Map<String, SctId> sctIds = getSctIds(componentIds); final Map<String, SctId> problemSctIds = ImmutableMap.copyOf(Maps.filterValues(sctIds, Predicates.<SctId>not(Predicates.or(SctId::isAssigned, SctId::isReserved, SctId::isAvailable)))); if (!problemSctIds.isEmpty()) { throw new SctIdStatusException( "Cannot release %s component IDs because they are not assigned, reserved, or already available.", problemSctIds); } final Map<String, SctId> assignedOrReservedSctIds = ImmutableMap .copyOf(Maps.filterValues(sctIds, Predicates.or(SctId::isAssigned, SctId::isReserved))); // XXX: It might be better to keep the last state change recorded in the index on these SctIds, but for now we remove them entirely removeSctIds(assignedOrReservedSctIds.keySet()); } @Override public void deprecate(final Set<String> componentIds) { LOGGER.debug("Deprecating {} component IDs.", componentIds.size()); final Map<String, SctId> sctIds = getSctIds(componentIds); final Map<String, SctId> problemSctIds = ImmutableMap.copyOf(Maps.filterValues(sctIds, Predicates.<SctId>not(Predicates.or(SctId::isAssigned, SctId::isPublished, SctId::isDeprecated)))); if (!problemSctIds.isEmpty()) { throw new SctIdStatusException( "Cannot deprecate %s component IDs because they are not assigned, published, or already deprecated.", problemSctIds); } final Map<String, SctId> assignedOrPublishedSctIds = ImmutableMap .copyOf(Maps.filterValues(sctIds, Predicates.or(SctId::isAssigned, SctId::isPublished))); for (final SctId sctId : assignedOrPublishedSctIds.values()) { sctId.setStatus(IdentifierStatus.DEPRECATED.getSerializedName()); } putSctIds(assignedOrPublishedSctIds); } @Override public void publish(final Set<String> componentIds) { LOGGER.debug("Publishing {} component IDs.", componentIds.size()); final Map<String, SctId> sctIds = getSctIds(componentIds); final Map<String, SctId> problemSctIds = ImmutableMap.copyOf(Maps.filterValues(sctIds, Predicates.<SctId>not(Predicates.or(SctId::isAssigned, SctId::isPublished)))); final Map<String, SctId> assignedSctIds = ImmutableMap.copyOf(Maps.filterValues(sctIds, SctId::isAssigned)); for (final SctId sctId : assignedSctIds.values()) { sctId.setStatus(IdentifierStatus.PUBLISHED.getSerializedName()); } putSctIds(assignedSctIds); if (!problemSctIds.isEmpty()) { LOGGER.warn( "Cannot publish the following component IDs because they are not assigned or already published: {}", problemSctIds); } } @Override public Map<String, SctId> getSctIds(final Set<String> componentIds) { final Query<SctId> getSctIdsQuery = Query.select(SctId.class) .where(Expressions.matchAny(DocumentMapping._ID, componentIds)).limit(componentIds.size()).build(); final Hits<SctId> existingIds = store.read(index -> index.search(getSctIdsQuery)); final Map<String, SctId> existingIdsMap = Maps.uniqueIndex(existingIds, SctId::getSctid); if (existingIdsMap.size() == componentIds.size()) { return existingIdsMap; } else { final Set<String> knownComponentIds = existingIdsMap.keySet(); final Set<String> difference = ImmutableSet.copyOf(Sets.difference(componentIds, knownComponentIds)); final ImmutableMap.Builder<String, SctId> resultBuilder = ImmutableMap.builder(); resultBuilder.putAll(existingIdsMap); for (final String componentId : difference) { resultBuilder.put(componentId, buildSctId(componentId, IdentifierStatus.AVAILABLE)); } return resultBuilder.build(); } } @Override public boolean importSupported() { return true; } private Set<String> generateIds(final String namespace, final ComponentCategory category, final int quantity) { final Set<String> generatedComponentIds = newLinkedHashSet(); // important to keep order of generated ids final int maxAttempts = getConfig().getMaxIdGenerationAttempts(); for (int attempt = 1; attempt <= maxAttempts && generatedComponentIds.size() != quantity; attempt++) { final int remainingQuantity = quantity - generatedComponentIds.size(); Set<String> newGeneratedComponentIds = doGenerateIds(namespace, category, remainingQuantity, attempt); // check this newly generated set for reservation Set<String> reservedIds = isReserved(newGeneratedComponentIds, generatedComponentIds); // remove IDs that are reserved from the newly generated set newGeneratedComponentIds.removeAll(reservedIds); // add new generated IDs to the results generatedComponentIds.addAll(newGeneratedComponentIds); } if (generatedComponentIds.size() != quantity) { final String namespaceValue = Strings.isNullOrEmpty(namespace) ? SnomedIdentifiers.INT_NAMESPACE : namespace; throw new BadRequestException( "Couldn't generate %s identifiers [%s, %s] in maximum (%s) number of attempts", quantity, category, namespaceValue, maxAttempts); } else { return ImmutableSet.<String>copyOf(generatedComponentIds); } } private Set<String> doGenerateIds(final String namespace, final ComponentCategory category, final int quantity, final int attempt) { // generate the item identifier (value can be a function of component category and namespace) return generationStrategy.generateItemIds(namespace, category, quantity, attempt).stream().map(itemId -> { final StringBuilder builder = new StringBuilder(); builder.append(itemId); // append namespace and the first digit of the partition-identifier if (Strings.isNullOrEmpty(namespace)) { builder.append('0'); } else { builder.append(namespace); builder.append('1'); } // append the second digit of the partition-identifier builder.append(category.ordinal()); // add Verhoeff check digit last builder.append(VerhoeffCheck.calculateChecksum(builder, false)); return builder.toString(); }).collect(Collectors.toCollection(Sets::newLinkedHashSet)); } private Set<String> isReserved(Set<String> ids, Set<String> currentGeneratedIds) { Set<String> remainingIdsToCheck = newHashSet(ids); final ImmutableSet.Builder<String> reservedIds = ImmutableSet.builder(); // check already generated set of IDs first Set<String> reservedByCurrentSet = Sets.intersection(remainingIdsToCheck, currentGeneratedIds); if (!reservedByCurrentSet.isEmpty()) { reservedIds.addAll(reservedByCurrentSet); remainingIdsToCheck.removeAll(reservedByCurrentSet); } // check local reservation service Set<String> reservedByService = getReservationService().isReserved(remainingIdsToCheck); if (!reservedByService.isEmpty()) { reservedIds.addAll(reservedByService); remainingIdsToCheck.removeAll(reservedByService); } // check the ID index to verify state of remaining IDs if (!remainingIdsToCheck.isEmpty()) { getSctIds(remainingIdsToCheck).forEach((id, sctId) -> { if (!sctId.isAvailable()) { reservedIds.add(id); } }); } return reservedIds.build(); } private SctId buildSctId(final String componentId, final IdentifierStatus status) { final SctId sctId = new SctId(); sctId.setSctid(componentId); sctId.setStatus(status.getSerializedName()); sctId.setSequence(SnomedIdentifiers.getItemId(componentId)); sctId.setNamespace(SnomedIdentifiers.getNamespace(componentId)); sctId.setPartitionId(SnomedIdentifiers.getPartitionId(componentId)); sctId.setCheckDigit(SnomedIdentifiers.getCheckDigit(componentId)); // TODO: Other attributes of SctId could also be set here return sctId; } private void putSctIds(final Map<String, SctId> ids) { store.write(index -> { index.putAll(ids); index.commit(); return null; }); } private void removeSctIds(final Set<String> ids) { store.write(index -> { index.removeAll(ImmutableMap.<Class<?>, Set<String>>of(SctId.class, ids)); index.commit(); return null; }); } @Override public void dispose() { if (disposed.compareAndSet(false, true)) { store.admin().close(); } } @Override public boolean isDisposed() { return disposed.get(); } }