| /* |
| * 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; |
| } |
| } |
| }); |
| } |
| } |