001/* 002 * Copyright (c) 2007-2022 The Cascading Authors. All Rights Reserved. 003 * 004 * Project and contact information: https://cascading.wensel.net/ 005 * 006 * This file is part of the Cascading project. 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020 021package cascading.local.tap.neo4j; 022 023import java.util.ArrayList; 024import java.util.LinkedHashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.function.Function; 029 030import com.fasterxml.jackson.databind.JsonNode; 031import com.fasterxml.jackson.databind.ObjectMapper; 032import iot.jcypher.database.internal.PlannerStrategy; 033import iot.jcypher.query.JcQuery; 034import iot.jcypher.query.api.IClause; 035import iot.jcypher.query.api.pattern.Node; 036import iot.jcypher.query.api.pattern.Relation; 037import iot.jcypher.query.factories.clause.MERGE; 038import iot.jcypher.query.factories.clause.ON_CREATE; 039import iot.jcypher.query.values.JcNode; 040import iot.jcypher.query.values.JcRelation; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044/** 045 * Current expects a fairly shallow document. 046 * <p> 047 * Any child Objects (map) will be flattened into the root using the name as a prefix for the key. 048 * e.g. `tag.start` and `tag.end`. 049 * <p> 050 * A nested array will be handed to Neo as an Array. So far Maps nested in an Array work fine. 051 */ 052public class Neo4jJSONStatement extends Neo4jStatement<JsonNode> 053 { 054 private static final Logger LOG = LoggerFactory.getLogger( Neo4jJSONStatement.class ); 055 056 private ObjectMapper objectMapper = new ObjectMapper(); 057 protected JSONGraphSpec graphSpec; 058 059 public Neo4jJSONStatement( JSONGraphSpec graphSpec ) 060 { 061 this.graphSpec = graphSpec; 062 } 063 064 @Override 065 public JcQuery getStatement( JsonNode json ) 066 { 067 List<IClause> clauses = new ArrayList<>(); 068 069 JcNode head = new JcNode( "n" ); 070 Node merge = MERGE.node( head ); 071 072 if( graphSpec.hasNodeLabel() ) 073 merge.label( graphSpec.getNodeLabel() ); 074 075 if( graphSpec.hasProperties() ) 076 applyProperties( json, merge, graphSpec.getProperties(), false ); 077 078 clauses.add( merge ); 079 080 Map<String, Object> propertyValues = asProperties( graphSpec.getValuesPointer().apply( json ) ); 081 082 for( Map.Entry<String, Object> entry : propertyValues.entrySet() ) 083 clauses.add( ON_CREATE.SET( head.property( entry.getKey() ) ).to( entry.getValue() ) ); 084 085 if( graphSpec.hasEdges() ) 086 { 087 Set<JSONGraphSpec.EdgeSpec> edges = graphSpec.getEdges(); 088 089 int count = 0; 090 091 for( JSONGraphSpec.EdgeSpec edge : edges ) 092 { 093 JcNode target = new JcNode( "t".concat( Integer.toString( count ) ) ); 094 Node targetMerge = MERGE.node( target ); 095 096 if( !edge.hasTargetLabel() && !edge.hasTargetProperties() ) 097 { 098 LOG.debug( "edge, {}, has no match patterns", edge.getEdgeType() ); 099 continue; 100 } 101 102 if( edge.hasTargetLabel() ) 103 targetMerge.label( edge.getTargetLabel() ); 104 105 if( edge.hasTargetProperties() ) 106 { 107 int propertiesFound = applyProperties( json, targetMerge, edge.getTargetProperties(), true ); 108 109 if( propertiesFound == 0 ) 110 { 111 LOG.debug( "edge, {}, has no match properties", edge.getEdgeType() ); 112 continue; 113 } 114 } 115 116 JcRelation relation = new JcRelation( "r".concat( Integer.toString( count ) ) ); 117 Relation intermediate = MERGE.node( head ).relation( relation ); 118 119 if( edge.hasEdgeType() ) 120 intermediate.type( edge.getEdgeType() ); 121 122 Node relationMerge = intermediate.out().node( target ); 123 124 clauses.add( targetMerge ); 125 clauses.add( relationMerge ); 126 127 count++; 128 } 129 } 130 131 JcQuery query = new JcQuery( PlannerStrategy.DEFAULT ); // force to default, otherwise warnings 132 133 query.setClauses( clauses.toArray( new IClause[ 0 ] ) ); 134 135 return query; 136 } 137 138 public int applyProperties( JsonNode json, Node merge, Map<String, Function<JsonNode, Object>> properties, boolean ignoreNull ) 139 { 140 int count = 0; 141 142 for( Map.Entry<String, Function<JsonNode, Object>> entry : properties.entrySet() ) 143 { 144 Object value = entry.getValue().apply( json ); 145 146 if( ignoreNull && value == null ) 147 continue; 148 else if( value == null ) 149 throw new IllegalStateException( "property: " + entry.getKey() + ", many not be null" ); 150 151 merge.property( entry.getKey() ).value( value ); 152 count++; 153 } 154 155 return count; 156 } 157 158 public Map<String, Object> asProperties( JsonNode node ) 159 { 160 Map<String, Object> result = new LinkedHashMap<>(); 161 162 Map<String, Object> map = objectMapper.convertValue( node, Map.class ); 163 164 nest( result, null, map ); 165 166 return result; 167 } 168 169 private void nest( Map<String, Object> result, String prefix, Map<String, Object> map ) 170 { 171 for( Map.Entry<String, Object> entry : map.entrySet() ) 172 { 173 String currentKey = cleanKey( entry.getKey() ); 174 Object value = entry.getValue(); 175 176 String nestedKey = prefix == null ? currentKey : prefix + "_" + currentKey; 177 178 if( value instanceof Map ) 179 nest( result, nestedKey, (Map<String, Object>) value ); 180 else 181 result.put( nestedKey, value ); // any nested lists stay lists 182 } 183 } 184 185 // todo: turn into a strategy owned by the GraphSpec instance 186 protected String cleanKey( String key ) 187 { 188 return key.replace( ':', '-' ) 189 .replace( '-', '_' ) 190 .replace( '/', '_' ) 191 .replace( '.', '_' ); 192 } 193 }