Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
apoc.uuid.UuidHandler Maven / Gradle / Ivy
package apoc.uuid;
import apoc.ApocConfig;
import apoc.ExtendedApocConfig;
import apoc.ExtendedSystemLabels;
import apoc.ExtendedSystemPropertyKeys;
import apoc.Pools;
import apoc.SystemPropertyKeys;
import apoc.util.Util;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.event.LabelEntry;
import org.neo4j.graphdb.event.PropertyEntry;
import org.neo4j.graphdb.event.TransactionData;
import org.neo4j.graphdb.event.TransactionEventListener;
import org.neo4j.graphdb.schema.ConstraintDefinition;
import org.neo4j.graphdb.schema.Schema;
import org.neo4j.kernel.internal.GraphDatabaseAPI;
import org.neo4j.kernel.lifecycle.LifecycleAdapter;
import org.neo4j.logging.Log;
import org.neo4j.scheduler.Group;
import org.neo4j.scheduler.JobHandle;
import org.neo4j.scheduler.JobScheduler;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static apoc.ExtendedApocConfig.APOC_UUID_FORMAT;
import static apoc.ExtendedSystemPropertyKeys.addToSetLabel;
import static apoc.ExtendedSystemPropertyKeys.propertyName;
import static apoc.util.SystemDbUtil.getLastUpdate;
import static apoc.uuid.Uuid.setExistingNodes;
import static apoc.uuid.UuidConfig.*;
public class UuidHandler extends LifecycleAdapter implements TransactionEventListener {
private final GraphDatabaseAPI db;
private final Log log;
private final DatabaseManagementService databaseManagementService;
private final ApocConfig apocConfig;
// Snapshot of uuid configuration per label/property.
// The containing map is immutable!
private final AtomicReference> labelAndPropertyNamesSnapshot = new AtomicReference<>(Map.of());
private final ExtendedApocConfig.UuidFormatType uuidFormat;
private final JobScheduler jobScheduler;
private final Pools pools;
private JobHandle refreshUuidHandle;
private volatile long lastUpdate;
public static final String APOC_UUID_REFRESH = "apoc.uuid.refresh";
public static final String NOT_ENABLED_ERROR = "UUID have not been enabled." +
" Set 'apoc.uuid.enabled=true' or 'apoc.uuid.enabled.%s=true' in your apoc.conf file located in the $NEO4J_HOME/conf/ directory.";
public UuidHandler(GraphDatabaseAPI db, DatabaseManagementService databaseManagementService, Log log, ApocConfig apocConfig, JobScheduler jobScheduler,
Pools pools) {
this.db = db;
this.databaseManagementService = databaseManagementService;
this.log = log;
this.apocConfig = apocConfig;
ExtendedApocConfig extendedApocConfig = ExtendedApocConfig.extendedApocConfig();
this.uuidFormat = extendedApocConfig.getEnumProperty(APOC_UUID_FORMAT, ExtendedApocConfig.UuidFormatType.class, ExtendedApocConfig.UuidFormatType.hex);
this.jobScheduler = jobScheduler;
this.pools = pools;
}
@Override
public void start() {
if (isEnabled()) {
refresh();
// not to cause breaking-change, with deprecated procedures we don't schedule the refresh()
Integer uuidRefresh = apocConfig.getConfig().getInteger(APOC_UUID_REFRESH, null);
if (uuidRefresh != null) {
refreshUuidHandle = jobScheduler.scheduleRecurring(Group.STORAGE_MAINTENANCE, () -> {
if (getLastUpdate(db.databaseName(), ExtendedSystemLabels.ApocUuidMeta) >= lastUpdate) {
refreshAndAdd();
}
},
uuidRefresh, uuidRefresh, TimeUnit.MILLISECONDS);
}
databaseManagementService.registerTransactionEventListener(db.databaseName(), this);
}
}
private boolean isEnabled() {
return UUIDHandlerNewProcedures.isEnabled(this.db.databaseName());
}
@Override
public void stop() {
if (isEnabled()) {
databaseManagementService.unregisterTransactionEventListener(db.databaseName(), this);
if (refreshUuidHandle != null) {
refreshUuidHandle.cancel();
}
}
}
private void checkAndRestoreUuidProperty(Iterable> nodeProperties, String label, String uuidProperty, TransactionData txData) {
checkAndRestoreUuidProperty(nodeProperties, label, uuidProperty, txData, null);
}
private void checkAndRestoreUuidProperty(Iterable> nodeProperties, String label, String uuidProperty, TransactionData txData, Predicate> predicate) {
if (nodeProperties.iterator().hasNext()) {
nodeProperties.forEach(nodePropertyEntry -> {
Node entity = nodePropertyEntry.entity();
if (txData.isDeleted(entity)) {
return;
}
if (predicate == null) {
if (entity.hasLabel(Label.label(label)) && nodePropertyEntry.key().equals(uuidProperty)) {
entity.setProperty(uuidProperty, nodePropertyEntry.previouslyCommittedValue());
}
} else {
if (entity.hasLabel(Label.label(label)) && nodePropertyEntry.key().equals(uuidProperty) && predicate.test(nodePropertyEntry)) {
entity.setProperty(uuidProperty, nodePropertyEntry.previouslyCommittedValue());
}
}
});
}
}
@Override
public Void beforeCommit(TransactionData txData, Transaction transaction, GraphDatabaseService databaseService) {
// assignedLabels handles both created nodes and set labels of existing nodes
Iterable> assignedNodeProperties = txData.assignedNodeProperties();
Iterable> removedNodeProperties = txData.removedNodeProperties();
labelAndPropertyNamesSnapshot.get().forEach((label, config) -> {
final String propertyName = config.getUuidProperty();
List nodes = config.isAddToSetLabels()
? StreamSupport.stream(txData.assignedLabels().spliterator(), false).map(LabelEntry::node).collect(Collectors.toList())
: IterableUtils.toList(txData.createdNodes());
try {
nodes.forEach(node -> {
if (node.hasLabel(Label.label(label)) && !node.hasProperty(propertyName)) {
String uuid = generateUuidValue();
node.setProperty(propertyName, uuid);
}
});
checkAndRestoreUuidProperty(assignedNodeProperties, label, propertyName, txData,
(nodePropertyEntry) -> nodePropertyEntry.value() == null || nodePropertyEntry.value().equals(""));
checkAndRestoreUuidProperty(removedNodeProperties, label, propertyName, txData);
} catch (Exception e) {
log.warn("Error executing uuid " + label + " in phase before", e);
}
});
return null;
}
@Override
public void afterCommit(TransactionData data, Void state, GraphDatabaseService databaseService) {
}
@Override
public void afterRollback(TransactionData data, Void state, GraphDatabaseService databaseService) {
}
private void checkEnabled() {
UUIDHandlerNewProcedures.checkEnabled(db.databaseName());
}
private String generateUuidValue() {
UUID uuid = UUID.randomUUID();
switch (uuidFormat) {
case base64:
return UuidUtil.generateBase64Uuid(uuid);
case hex:
default:
return uuid.toString();
}
}
public void checkConstraintUuid(Transaction tx, String label, String propertyName) {
Schema schema = tx.schema();
Stream constraintDefinitionStream = StreamSupport.stream(schema.getConstraints(Label.label(label)).spliterator(), false);
boolean exists = constraintDefinitionStream.anyMatch(constraint -> {
Stream streamPropertyKeys = StreamSupport.stream(constraint.getPropertyKeys().spliterator(), false);
return streamPropertyKeys.anyMatch(property -> property.equals(propertyName));
});
if (!exists) {
String error = String.format("`CREATE CONSTRAINT FOR (%s:%s) REQUIRE %s.%s IS UNIQUE`",
label.toLowerCase(), label, label.toLowerCase(), propertyName);
throw new RuntimeException("No constraint found for label: " + label + ", please add the constraint with the following : " + error);
}
}
public void add(Transaction tx, String label, UuidConfig config) {
checkEnabled();
final String propertyName = config.getUuidProperty();
checkConstraintUuid(tx, label, propertyName);
try (Transaction sysTx = apocConfig.getSystemDb().beginTx()) {
Node node = Util.mergeNode(sysTx, ExtendedSystemLabels.ApocUuid, null,
Pair.of(SystemPropertyKeys.database.name(), db.databaseName()),
Pair.of(ExtendedSystemPropertyKeys.label.name(), label),
Pair.of(ExtendedSystemPropertyKeys.propertyName.name(), propertyName)
);
node.setProperty(addToSetLabel.name(), config.isAddToSetLabels());
sysTx.commit();
}
refresh();
}
public Map list() {
checkEnabled();
return labelAndPropertyNamesSnapshot.get();
}
public synchronized void refreshAndAdd() {
final var start = System.currentTimeMillis();
final var localCache = provisionalRefresh();
if (Util.isWriteableInstance(db)) {
// add to existing nodes
localCache.forEach((label, conf) -> {
// auto-create uuid constraint
if (conf.isCreateConstraint()) {
try {
String queryConst = String.format("CREATE CONSTRAINT IF NOT EXISTS FOR (n:%s) REQUIRE (n.%s) IS UNIQUE",
Util.quote(label),
Util.quote(conf.getUuidProperty())
);
db.executeTransactionally(queryConst);
} catch (Exception e) {
log.error("Error during uuid constraint auto-creation: " + e.getMessage());
}
conf.setCreateConstraint(false);
}
if (conf.isAddToExistingNodes()) {
try {
Map result = setExistingNodes(db, pools, label, conf);
String logBatchResult = String.format(
"Result of batch computation obtained from existing nodes for UUID handler with label `%s`: \n %s",
label, result);
log.info(logBatchResult);
} catch (Exception e) {
String errMsg;
if (e.getMessage().contains( "There is no procedure with the name `apoc.periodic.iterate` registered for this database instance" )) {
errMsg = "apoc core needs to be installed when using apoc.uuid.install with the flag addToExistingNodes = true";
} else {
errMsg = e.getMessage();
}
log.error("Error during uuid set to existing nodes: " + errMsg);
}
conf.setAddToExistingNodes(false);
}
});
}
labelAndPropertyNamesSnapshot.set(localCache);
lastUpdate = start;
}
public Map provisionalRefresh() {
try (Transaction tx = apocConfig.getSystemDb().beginTx()) {
return tx.findNodes(ExtendedSystemLabels.ApocUuid, SystemPropertyKeys.database.name(), db.databaseName())
.stream()
.collect(Collectors.toUnmodifiableMap(
node -> (String) node.getProperty(ExtendedSystemPropertyKeys.label.name()),
node -> new UuidConfig(Map.of(
UUID_PROPERTY_KEY, node.getProperty(ExtendedSystemPropertyKeys.propertyName.name()),
ADD_TO_SET_LABELS_KEY, node.getProperty(ExtendedSystemPropertyKeys.addToSetLabel.name(), false),
ADD_TO_EXISTING_NODES_KEY, node.getProperty(ExtendedSystemPropertyKeys.addToExistingNodes.name(), false)
))
));
}
}
public void refresh() {
final var start = System.currentTimeMillis();
// Note, there is a race condition here where lastUpdate can become larger than the last update
// if two threads are refreshing at the same time.
labelAndPropertyNamesSnapshot.set(provisionalRefresh());
lastUpdate = start;
}
public synchronized UuidConfig remove(String label) {
final var oldValue = labelAndPropertyNamesSnapshot.get().get(label);
try (Transaction tx = apocConfig.getSystemDb().beginTx()) {
tx.findNodes(ExtendedSystemLabels.ApocUuid, SystemPropertyKeys.database.name(), db.databaseName(),
ExtendedSystemPropertyKeys.label.name(), label)
.forEachRemaining(node -> node.delete());
tx.commit();
}
refresh();
return oldValue;
}
public synchronized Map removeAll() {
final var retval = labelAndPropertyNamesSnapshot.get();
labelAndPropertyNamesSnapshot.set(Map.of());
try (Transaction tx = apocConfig.getSystemDb().beginTx()) {
tx.findNodes(ExtendedSystemLabels.ApocUuid, SystemPropertyKeys.database.name(), db.databaseName() )
.forEachRemaining(node -> node.delete());
tx.commit();
}
refresh();
return retval;
}
}