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  }