blob: 83a4f680053f9454cc59c27213c936894fecf83c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.syncope.core.persistence.neo4j.content;
import jakarta.xml.bind.DatatypeConverter;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.syncope.core.persistence.api.utils.FormatUtils;
import org.apache.syncope.core.persistence.common.content.AbstractContentLoaderHandler;
import org.apache.syncope.core.persistence.neo4j.entity.Neo4jDerSchema;
import org.apache.syncope.core.persistence.neo4j.entity.Neo4jImplementationRelationship;
import org.apache.syncope.core.persistence.neo4j.entity.Neo4jPlainSchema;
import org.apache.syncope.core.persistence.neo4j.entity.Neo4jSchema;
import org.apache.syncope.core.persistence.neo4j.entity.Neo4jVirSchema;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jAccessPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jAccountPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jAttrReleasePolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jAuthPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jPasswordPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jPropagationPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jPullPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jPushPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jTicketExpirationPolicy;
import org.apache.syncope.core.persistence.neo4j.entity.task.Neo4jMacroTask;
import org.apache.syncope.core.persistence.neo4j.entity.task.Neo4jMacroTaskCommandRelationship;
import org.apache.syncope.core.persistence.neo4j.entity.task.Neo4jProvisioningTask;
import org.apache.syncope.core.persistence.neo4j.entity.task.Neo4jPullTask;
import org.apache.syncope.core.persistence.neo4j.entity.task.Neo4jPushTask;
import org.apache.syncope.core.persistence.neo4j.entity.task.Neo4jSchedTask;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.springframework.core.env.Environment;
import org.springframework.data.neo4j.core.mapping.GraphPropertyDescription;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.NodeDescription;
import org.xml.sax.Attributes;
/**
* SAX handler for generating CREATE statements out of given XML file.
*/
public class ContentLoaderHandler extends AbstractContentLoaderHandler {
protected static record Node(String id, Map<String, Object> props) {
}
protected static record Relationship(String leftId, String rightId, String type, String index) {
}
protected static record Query(String statement, Map<String, Object> props) {
}
protected static String nodelabels(final String primaryLabel) {
switch (primaryLabel) {
case Neo4jPlainSchema.NODE -> {
return Neo4jPlainSchema.NODE + ":" + Neo4jSchema.NODE;
}
case Neo4jDerSchema.NODE -> {
return Neo4jDerSchema.NODE + ":" + Neo4jSchema.NODE;
}
case Neo4jVirSchema.NODE -> {
return Neo4jVirSchema.NODE + ":" + Neo4jSchema.NODE;
}
case Neo4jAccessPolicy.NODE -> {
return Neo4jAccessPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jAccountPolicy.NODE -> {
return Neo4jAccountPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jAttrReleasePolicy.NODE -> {
return Neo4jAttrReleasePolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jAuthPolicy.NODE -> {
return Neo4jAuthPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jPasswordPolicy.NODE -> {
return Neo4jPasswordPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jPropagationPolicy.NODE -> {
return Neo4jPropagationPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jPushPolicy.NODE -> {
return Neo4jPushPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jPullPolicy.NODE -> {
return Neo4jPullPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jTicketExpirationPolicy.NODE -> {
return Neo4jTicketExpirationPolicy.NODE + ":" + Neo4jPolicy.NODE;
}
case Neo4jPushTask.NODE -> {
return Neo4jPushTask.NODE + ":" + Neo4jProvisioningTask.NODE + ":" + Neo4jSchedTask.NODE;
}
case Neo4jPullTask.NODE -> {
return Neo4jPullTask.NODE + ":" + Neo4jProvisioningTask.NODE + ":" + Neo4jSchedTask.NODE;
}
case Neo4jMacroTask.NODE -> {
return Neo4jMacroTask.NODE + ":" + Neo4jSchedTask.NODE;
}
default -> {
return primaryLabel;
}
}
}
protected static String escape(final String k) {
return k.startsWith("plainAttrs.") ? k.replace('.', '_') : k;
}
protected final Driver driver;
protected final Neo4jMappingContext mappingContext;
public ContentLoaderHandler(
final Driver driver,
final Neo4jMappingContext mappingContext,
final String rootElement,
final boolean continueOnError,
final Environment env) {
super(rootElement, continueOnError, env);
this.driver = driver;
this.mappingContext = mappingContext;
}
@Override
protected void fetch(final Attributes atts) {
try (Session session = driver.session()) {
String value = session.run(atts.getValue("query")).single().get(0).asString();
String key = atts.getValue("key");
fetches.put(key, value);
} catch (Exception e) {
LOG.error("While running '{}'", atts.getValue("query"), e);
}
}
protected Optional<Node> parseNode(final NodeDescription<?> nodeDesc, final Attributes atts) {
String id = null;
Map<String, Object> props = new HashMap<>();
for (int i = 0; i < atts.getLength(); i++) {
String originalName = atts.getQName(i);
String originalValue = atts.getValue(i);
if ("id".equalsIgnoreCase(originalName)) {
id = originalValue;
props.put("id", originalValue);
} else {
String name = nodeDesc.getGraphProperties().stream().
filter(prop -> prop.getPropertyName().equalsIgnoreCase(originalName)).
map(GraphPropertyDescription::getPropertyName).
findFirst().orElseGet(() -> originalName.startsWith("plainAttrs.") ? originalName : null);
if (name == null) {
LOG.error("Property {} not matching for {}", originalName, nodeDesc.getPrimaryLabel());
continue;
}
Class<?> type = nodeDesc.getGraphProperties().stream().
filter(prop -> prop.getPropertyName().equalsIgnoreCase(name)).
findFirst().map(GraphPropertyDescription::getActualType).
orElseGet(() -> {
if (!name.startsWith("plainAttrs.")) {
LOG.warn("No type found for property {}#{}", nodeDesc.getPrimaryLabel(), name);
}
return String.class;
});
String value = paramSubstitutor.replace(atts.getValue(i));
if (value == null) {
LOG.warn("Variable ${} could not be resolved", atts.getValue(i));
value = atts.getValue(i);
}
value = StringEscapeUtils.unescapeXml(value);
if (int.class.isAssignableFrom(type) || Integer.class.isAssignableFrom(type)) {
try {
props.put(name, Integer.valueOf(value));
} catch (NumberFormatException e) {
LOG.error("Unparsable Integer '{}'", value);
}
} else if (long.class.isAssignableFrom(type) || Long.class.isAssignableFrom(type)) {
try {
props.put(name, Long.valueOf(value));
} catch (NumberFormatException e) {
LOG.error("Unparsable Long '{}'", value);
}
} else if (float.class.isAssignableFrom(type) || Float.class.isAssignableFrom(type)) {
try {
props.put(name, Float.valueOf(value));
} catch (NumberFormatException e) {
LOG.error("Unparsable Float '{}'", value);
}
} else if (double.class.isAssignableFrom(type) || Double.class.isAssignableFrom(type)) {
try {
props.put(name, Double.valueOf(value));
} catch (NumberFormatException e) {
LOG.error("Unparsable Double '{}'", value);
}
} else if (Date.class.isAssignableFrom(type) || OffsetDateTime.class.isAssignableFrom(type)) {
try {
props.put(name, FormatUtils.parseDate(value));
} catch (DateTimeParseException e) {
LOG.error("Unparsable Date '{}'", value);
}
} else if (boolean.class.isAssignableFrom(type) || Boolean.class.isAssignableFrom(type)) {
props.put(name, "1".equals(value) ? Boolean.TRUE : Boolean.FALSE);
} else if (byte[].class.isAssignableFrom(type)) {
try {
props.put(name, DatatypeConverter.parseHexBinary(value));
} catch (IllegalArgumentException e) {
LOG.warn("Error decoding hex string to specify a blob parameter", e);
}
}
if (!props.containsKey(name)) {
props.put(name, value);
}
}
}
return id == null ? Optional.empty() : Optional.of(new Node(id, props));
}
protected Optional<Relationship> parseRelationship(
final NodeDescription<?> nodeDesc, final String rightNode, final Attributes atts) {
String left = null;
String right = null;
String type = null;
String index = null;
for (int i = 0; i < atts.getLength(); i++) {
if ("left".equalsIgnoreCase(atts.getQName(i))) {
left = atts.getValue(i);
} else if ("right".equalsIgnoreCase(atts.getQName(i))) {
right = atts.getValue(i);
} else if ("type".equalsIgnoreCase(atts.getQName(i))) {
type = atts.getValue(i);
} else if ("index".equalsIgnoreCase(atts.getQName(i))) {
index = atts.getValue(i);
}
}
if (left == null || right == null) {
LOG.warn("Could not find left and/or right attribute in {}_{}", nodeDesc.getPrimaryLabel(), rightNode);
return Optional.empty();
}
String leftId = left;
String rightId = right;
String relType = type;
String indexValue = index;
return nodeDesc.getRelationships().stream().
filter(rel -> rightNode.equals(rel.getTarget().getPrimaryLabel())
&& (relType == null || relType.equals(rel.getType()))).
findFirst().map(rel -> new Relationship(
leftId,
rightId,
rel.getType(),
Optional.ofNullable(rel.getRelationshipPropertiesEntity()).
filter(e -> Neo4jImplementationRelationship.class.getSimpleName().equals(e.getPrimaryLabel())
|| Neo4jMacroTaskCommandRelationship.class.getSimpleName().equals(e.getPrimaryLabel())).
map(e -> indexValue).orElse(null)));
}
@Override
protected void create(final String qName, final Attributes atts) {
Optional<Query> query;
if (qName.contains("_")) {
String[] split = qName.split("_");
query = parseRelationship(mappingContext.getNodeDescription(split[0]), split[1], atts).
map(rel -> new Query(
"MATCH (a:" + split[0] + " {id: '" + rel.leftId() + "'}), "
+ "(b:" + split[1] + " {id: '" + rel.rightId() + "'}) "
+ "CREATE (a)-"
+ "[:" + rel.type() + (rel.index() == null ? "" : " {index: " + rel.index() + "}") + "]->(b)",
Map.of()));
} else {
query = parseNode(mappingContext.getNodeDescription(qName), atts).map(node -> {
StringBuilder q = new StringBuilder("CREATE (n:").append(nodelabels(qName)).append(" {");
q.append(node.props().entrySet().stream().
map(e -> "`" + e.getKey() + "`" + ": $" + escape(e.getKey())).
collect(Collectors.joining(", ")));
q.append("})");
return new Query(q.toString(), node.props().entrySet().stream().
collect(Collectors.toMap(e -> escape(e.getKey()), Map.Entry::getValue)));
});
}
query.ifPresent(q -> {
LOG.debug("About to run: {}", q);
try (Session session = driver.session()) {
session.run(q.statement(), q.props());
} catch (Exception e) {
LOG.error("While processing {}", qName, e);
if (!continueOnError) {
throw e;
}
}
});
}
}