package bi.know.kettle.neo4j.steps.output; import bi.know.kettle.neo4j.shared.MetaStoreUtil; import bi.know.kettle.neo4j.shared.NeoConnectionUtils; import bi.know.kettle.neo4j.steps.BaseNeoStep; import org.apache.commons.lang.StringEscapeUtils; import org.apache.commons.lang.StringUtils; import org.neo4j.driver.Result; import org.neo4j.driver.summary.Notification; import org.neo4j.driver.summary.ResultSummary; import org.neo4j.kettle.core.GraphUsage; import org.neo4j.kettle.core.data.GraphData; import org.neo4j.kettle.core.data.GraphNodeData; import org.neo4j.kettle.core.data.GraphPropertyData; import org.neo4j.kettle.core.data.GraphPropertyDataType; import org.neo4j.kettle.core.data.GraphRelationshipData; import org.neo4j.kettle.model.GraphPropertyType; import org.pentaho.di.core.Const; import org.pentaho.di.core.exception.KettleException; import org.pentaho.di.core.exception.KettleValueException; import org.pentaho.di.core.row.RowDataUtil; import org.pentaho.di.core.row.RowMetaInterface; import org.pentaho.di.core.row.ValueMetaInterface; import org.pentaho.di.core.util.StringUtil; import org.pentaho.di.trans.Trans; import org.pentaho.di.trans.TransMeta; import org.pentaho.di.trans.step.StepDataInterface; import org.pentaho.di.trans.step.StepInterface; import org.pentaho.di.trans.step.StepMeta; import org.pentaho.di.trans.step.StepMetaInterface; import org.pentaho.metastore.api.exceptions.MetaStoreException; import org.pentaho.metastore.stores.xml.XmlMetaStore; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; public class Neo4JOutput extends BaseNeoStep implements StepInterface { private static Class<?> PKG = Neo4JOutput.class; // for i18n purposes, needed by Translator2!! private Neo4JOutputMeta meta; private Neo4JOutputData data; public Neo4JOutput( StepMeta s, StepDataInterface stepDataInterface, int c, TransMeta t, Trans dis ) { super( s, stepDataInterface, c, t, dis ); } /** * TODO: * 1. option to do NODE CREATE/NODE UPDATE (merge default?) * 2. optional commit size * 3. option to return node id? */ public boolean processRow( StepMetaInterface smi, StepDataInterface sdi ) throws KettleException { meta = (Neo4JOutputMeta) smi; data = (Neo4JOutputData) sdi; Object[] row = getRow(); if ( row == null ) { setOutputDone(); return false; } if ( first ) { first = false; data.outputRowMeta = getInputRowMeta().clone(); meta.getFields( data.outputRowMeta, getStepname(), null, null, this, repository, metaStore ); data.fieldNames = data.outputRowMeta.getFieldNames(); data.fromNodePropIndexes = new int[ meta.getFromNodeProps().length ]; data.fromNodePropTypes = new GraphPropertyType[ meta.getFromNodeProps().length ]; for ( int i = 0; i < meta.getFromNodeProps().length; i++ ) { data.fromNodePropIndexes[ i ] = data.outputRowMeta.indexOfValue( meta.getFromNodeProps()[ i ] ); if ( data.fromNodePropIndexes[ i ] < 0 ) { throw new KettleException( "From node: Unable to find field '" + meta.getFromNodeProps()[ i ] + "' for property name '" + meta.getFromNodePropNames()[ i ] + "'" ); } data.fromNodePropTypes[ i ] = GraphPropertyType.parseCode( meta.getFromNodePropTypes()[ i ] ); } data.fromNodeLabelIndexes = new int[ meta.getFromNodeLabels().length ]; for ( int i = 0; i < meta.getFromNodeLabels().length; i++ ) { data.fromNodeLabelIndexes[ i ] = data.outputRowMeta.indexOfValue( meta.getFromNodeLabels()[ i ] ); if ( data.fromNodeLabelIndexes[ i ] < 0 && StringUtils.isEmpty( meta.getFromNodeLabelValues()[ i ] ) ) { throw new KettleException( "From node : please provide either a static label value or a field name to determine the label" ); } } data.toNodePropIndexes = new int[ meta.getToNodeProps().length ]; data.toNodePropTypes = new GraphPropertyType[ meta.getToNodeProps().length ]; for ( int i = 0; i < meta.getToNodeProps().length; i++ ) { data.toNodePropIndexes[ i ] = data.outputRowMeta.indexOfValue( meta.getToNodeProps()[ i ] ); data.toNodePropTypes[ i ] = GraphPropertyType.parseCode( meta.getToNodePropTypes()[ i ] ); } data.toNodeLabelIndexes = new int[ meta.getToNodeLabels().length ]; for ( int i = 0; i < meta.getToNodeLabels().length; i++ ) { data.toNodeLabelIndexes[ i ] = data.outputRowMeta.indexOfValue( meta.getToNodeLabels()[ i ] ); if ( data.toNodeLabelIndexes[ i ] < 0 && StringUtils.isEmpty( meta.getToNodeLabelValues()[ i ] ) ) { throw new KettleException( "To node : please provide either a static label value or a field name to determine the label" ); } } data.relPropIndexes = new int[ meta.getRelProps().length ]; data.relPropTypes = new GraphPropertyType[ meta.getRelProps().length ]; for ( int i = 0; i < meta.getRelProps().length; i++ ) { data.relPropIndexes[ i ] = data.outputRowMeta.indexOfValue( meta.getRelProps()[ i ] ); data.relPropTypes[ i ] = GraphPropertyType.parseCode( meta.getRelPropTypes()[ i ] ); } data.relationshipIndex = data.outputRowMeta.indexOfValue( meta.getRelationship() ); data.fromLabelValues = new String[ meta.getFromNodeLabelValues().length ]; for ( int i = 0; i < meta.getFromNodeLabelValues().length; i++ ) { data.fromLabelValues[ i ] = environmentSubstitute( meta.getFromNodeLabelValues()[ i ] ); } data.toLabelValues = new String[ meta.getToNodeLabelValues().length ]; for ( int i = 0; i < meta.getToNodeLabelValues().length; i++ ) { data.toLabelValues[ i ] = environmentSubstitute( meta.getToNodeLabelValues()[ i ] ); } data.relationshipLabelValue = environmentSubstitute( meta.getRelationshipValue() ); data.unwindList = new ArrayList<>(); data.dynamicFromLabels = determineDynamicLabels( meta.getFromNodeLabels() ); data.dynamicToLabels = determineDynamicLabels( meta.getToNodeLabels() ); data.dynamicRelLabel = StringUtils.isNotEmpty( meta.getRelationship() ); data.previousFromLabelsClause = null; data.previousToLabelsClause = null; data.previousRelationshipLabel = null; // Calculate the operation types // data.fromOperationType = OperationType.MERGE; data.toOperationType = OperationType.MERGE; data.relOperationType = OperationType.MERGE; if ( meta.isUsingCreate() ) { data.fromOperationType = OperationType.CREATE; data.toOperationType = OperationType.CREATE; data.relOperationType = OperationType.CREATE; } if ( meta.isOnlyCreatingRelationships() ) { data.fromOperationType = OperationType.MATCH; data.toOperationType = OperationType.MATCH; data.relOperationType = OperationType.CREATE; } // No 'From' Node activity? // if ( meta.getFromNodeLabels().length == 0 && meta.getFromNodeLabelValues().length == 0 ) { data.fromOperationType = OperationType.NONE; } // No 'To' Node activity? // if ( meta.getToNodeLabels().length == 0 && meta.getToNodeLabelValues().length == 0 ) { data.toOperationType = OperationType.NONE; } // No relationship activity? // if ( StringUtils.isEmpty( meta.getRelationship() ) && StringUtils.isEmpty( meta.getRelationshipValue() ) ) { data.relOperationType = OperationType.NONE; } // Create a session // if ( meta.isReturningGraph() ) { log.logBasic( "Writing to output graph field, not to Neo4j" ); } else { data.session = data.neoConnection.getSession( log ); // Create indexes for the primary properties of the From and To nodes // if ( meta.isCreatingIndexes() ) { try { createNodePropertyIndexes( meta, data, getInputRowMeta(), row ); } catch ( KettleException e ) { log.logError( "Unable to create indexes", e ); return false; } } } } if ( meta.isReturningGraph() ) { // Let the next steps handle writing to Neo4j // outputGraphValue( getInputRowMeta(), row ); } else { boolean changedLabel = calculateLabelsAndDetectChanges( row ); if ( changedLabel ) { emptyUnwindList( changedLabel ); } // Add rows to the unwind list. Just put all the properties from the nodes and relationship in there // This could lead to property name collisions so we prepend the properties in the list with alias and underscore // Map<String, Object> propsMap = new HashMap<>(); if ( data.fromOperationType != OperationType.NONE ) { addPropertiesToMap( propsMap, "f", data.fromNodePropIndexes, getInputRowMeta(), row, meta.getFromNodePropNames(), data.fromNodePropTypes ); } if ( data.toOperationType != OperationType.NONE ) { addPropertiesToMap( propsMap, "t", data.toNodePropIndexes, getInputRowMeta(), row, meta.getToNodePropNames(), data.toNodePropTypes ); } if ( data.relOperationType != OperationType.NONE ) { addPropertiesToMap( propsMap, "r", data.relPropIndexes, getInputRowMeta(), row, meta.getRelPropNames(), data.relPropTypes ); } data.unwindList.add( propsMap ); if ( data.unwindList.size() >= data.batchSize ) { emptyUnwindList( changedLabel ); } // Simply pass on the current row . // putRow( data.outputRowMeta, row ); // Remember the previous labels // data.previousFromLabelsClause = data.fromLabelsClause; data.previousToLabelsClause = data.toLabelsClause; data.previousRelationshipLabel = data.relationshipLabel; } return true; } private void addPropertiesToMap( Map<String, Object> rowMap, String alias, int[] nodePropIndexes, RowMetaInterface rowMeta, Object[] row, String[] nodePropNames, GraphPropertyType[] propertyTypes ) throws KettleValueException { // Add all the node properties for the current row to the rowMap // for ( int i = 0; i < nodePropIndexes.length; i++ ) { ValueMetaInterface valueMeta = rowMeta.getValueMeta( nodePropIndexes[ i ] ); Object valueData = row[ nodePropIndexes[ i ] ]; GraphPropertyType propertyType = propertyTypes[ i ]; Object neoValue = propertyType.convertFromKettle( valueMeta, valueData ); String propName = "p" + nodePropIndexes[ i ]; rowMap.put( propName, neoValue ); } } private void emptyUnwindList( boolean changedLabel ) throws KettleException { Map<String, Object> properties = Collections.singletonMap( "props", data.unwindList ); if ( data.cypher == null || changedLabel ) { StringBuilder cypher = new StringBuilder(); cypher.append( "UNWIND $props as pr " ).append( Const.CR ); // The cypher for the 'from' node: // boolean takePreviousFrom = data.dynamicFromLabels && changedLabel && data.previousFromLabelsClause != null; String fromLabelClause = takePreviousFrom ? data.previousFromLabelsClause : data.fromLabelsClause; String fromMatchClause = getMatchClause( meta.getFromNodePropNames(), meta.getFromNodePropPrimary(), data.fromNodePropIndexes, "f" ); switch ( data.fromOperationType ) { case NONE: break; case CREATE: cypher .append( "CREATE( " ) .append( fromLabelClause ) .append( " " ) .append( fromMatchClause ) .append( ") " ) .append( Const.CR ) ; String setClause = getSetClause( false, "f", meta.getFromNodePropNames(), meta.getFromNodePropPrimary(), data.fromNodePropIndexes ); if ( StringUtils.isNotEmpty( setClause ) ) { cypher .append( setClause ) .append( Const.CR ) ; } updateUsageMap( data.fromLabels, GraphUsage.NODE_CREATE ); break; case MERGE: cypher .append( "MERGE( " ) .append( fromLabelClause ) .append( " " ) .append( fromMatchClause ) .append( ") " ) .append( Const.CR ) ; setClause = getSetClause( false, "f", meta.getFromNodePropNames(), meta.getFromNodePropPrimary(), data.fromNodePropIndexes ); if ( StringUtils.isNotEmpty( setClause ) ) { cypher .append( setClause ) .append( Const.CR ) ; } updateUsageMap( data.fromLabels, GraphUsage.NODE_UPDATE ); break; case MATCH: cypher .append( "MATCH( " ) .append( fromLabelClause ) .append( " " ) .append( fromMatchClause ) .append( ") " ) .append( Const.CR ) ; updateUsageMap( data.toLabels, GraphUsage.NODE_READ ); break; default: throw new KettleException( "Unsupported operation type for the 'from' node: " + data.fromOperationType ); } // The cypher for the 'to' node: // boolean takePreviousTo = data.dynamicToLabels && changedLabel; String toLabelsClause = takePreviousTo ? data.previousToLabelsClause : data.toLabelsClause; String toMatchClause = getMatchClause( meta.getToNodePropNames(), meta.getToNodePropPrimary(), data.toNodePropIndexes, "f" ); switch ( data.toOperationType ) { case NONE: break; case CREATE: cypher .append( "CREATE( " ) .append( toLabelsClause ) .append( " " ) .append( toMatchClause ) .append( ") " ) .append( Const.CR ) ; String setClause = getSetClause( false, "t", meta.getToNodePropNames(), meta.getToNodePropPrimary(), data.toNodePropIndexes ); if ( StringUtils.isNotEmpty( setClause ) ) { cypher .append( setClause ) .append( Const.CR ) ; } updateUsageMap( data.toLabels, GraphUsage.NODE_CREATE ); break; case MERGE: cypher .append( "MERGE( " ) .append( toLabelsClause ) .append( " " ) .append( toMatchClause ) .append( ") " ) .append( Const.CR ) ; setClause = getSetClause( false, "t", meta.getToNodePropNames(), meta.getToNodePropPrimary(), data.toNodePropIndexes ); if ( StringUtils.isNotEmpty( setClause ) ) { cypher .append( setClause ) .append( Const.CR ) ; } updateUsageMap( data.toLabels, GraphUsage.NODE_UPDATE ); break; case MATCH: cypher .append( "MATCH( " ) .append( toLabelsClause ) .append( " " ) .append( toMatchClause ) .append( ") " ) .append( Const.CR ) ; updateUsageMap( data.toLabels, GraphUsage.NODE_READ ); break; default: throw new KettleException( "Unsupported operation type for the 'to' node: " + data.toOperationType ); } // The cypher for the relationship: // String relationshipSetClause = getSetClause( false, "r", meta.getRelPropNames(), new boolean[ meta.getRelPropNames().length ], data.relPropIndexes ); switch ( data.relOperationType ) { case NONE: break; case MERGE: cypher .append( "MERGE (f)-[" ) .append( "r:" ) .append( data.relationshipLabel ) .append( "]->(t) " ) .append( Const.CR ) .append( relationshipSetClause ) .append( Const.CR ) ; updateUsageMap( Arrays.asList( data.relationshipLabel ), GraphUsage.RELATIONSHIP_UPDATE ); ; break; case CREATE: cypher .append( "CREATE (f)-[" ) .append( "r:" ) .append( data.relationshipLabel ) .append( "]->(t) " ) .append( Const.CR ) .append( getSetClause( false, "r", meta.getRelPropNames(), new boolean[ meta.getRelPropNames().length ], data.relPropIndexes ) ) .append( Const.CR ) ; updateUsageMap( Arrays.asList( data.relationshipLabel ), GraphUsage.RELATIONSHIP_CREATE ); break; } data.cypher = cypher.toString(); } // OK now we have the cypher statement, we can execute it... // if ( isDebug() ) { logDebug( "Running Cypher: " + data.cypher ); logDebug( "properties list size : " + data.unwindList.size() ); } // Run it always without beginTransaction()... // Result result = data.session.writeTransaction( tx -> tx.run( data.cypher, properties ) ); processSummary( result ); setLinesOutput( getLinesOutput() + data.unwindList.size() ); // Clear the list // data.unwindList.clear(); } private String getMatchClause( String[] propertyNames, boolean[] propertyPrimary, int[] nodePropIndexes, String alias ) { StringBuilder clause = new StringBuilder(); for ( int i = 0; i < propertyNames.length; i++ ) { if ( propertyPrimary[ i ] ) { if ( clause.length() > 0 ) { clause.append( ", " ); } clause .append( propertyNames[ i ] ) .append( ": pr.p" ) .append( nodePropIndexes[ i ] ) ; } } if ( clause.length() == 0 ) { return ""; } else { return "{ " + clause + " }"; } } private String getSetClause( boolean allProperties, String alias, String[] propertyNames, boolean[] propertyPrimary, int[] nodePropIndexes ) { StringBuilder clause = new StringBuilder(); for ( int i = 0; i < propertyNames.length; i++ ) { if ( allProperties || !propertyPrimary[ i ] ) { if ( clause.length() > 0 ) { clause.append( ", " ); } clause .append( alias ) .append( "." ) .append( propertyNames[ i ] ) .append( "= pr.p" ) .append( nodePropIndexes[ i ] ) ; } } if ( clause.length() == 0 ) { return ""; } else { return "SET " + clause; } } private boolean calculateLabelsAndDetectChanges( Object[] row ) throws KettleException { boolean changedLabel = false; if ( data.fromOperationType != OperationType.NONE ) { if ( data.fromLabelsClause == null || data.dynamicFromLabels ) { List<String> fLabels = getNodeLabels( meta.getFromNodeLabels(), data.fromLabelValues, getInputRowMeta(), row, data.fromNodeLabelIndexes ); data.fromLabelsClause = getLabels( "f", fLabels ); } if ( data.dynamicFromLabels && data.previousFromLabelsClause != null && data.fromLabelsClause != null ) { if ( !data.fromLabelsClause.equals( data.previousFromLabelsClause ) ) { changedLabel = true; } } } if ( data.toOperationType != OperationType.NONE ) { if ( data.toLabelsClause == null || data.dynamicToLabels ) { List<String> tLabels = getNodeLabels( meta.getToNodeLabels(), data.toLabelValues, getInputRowMeta(), row, data.toNodeLabelIndexes ); data.toLabelsClause = getLabels( "t", tLabels ); } if ( data.dynamicToLabels && data.previousToLabelsClause != null && data.toLabelsClause != null ) { if ( !data.toLabelsClause.equals( data.previousToLabelsClause ) ) { changedLabel = true; } } } if ( data.relOperationType != OperationType.NONE ) { if ( data.dynamicRelLabel ) { data.relationshipLabel = getInputRowMeta().getString( row, data.relationshipIndex ); } if ( StringUtils.isEmpty( data.relationshipLabel ) && StringUtils.isNotEmpty( data.relationshipLabelValue ) ) { data.relationshipLabel = data.relationshipLabelValue; } if ( data.dynamicRelLabel && data.previousRelationshipLabel != null && data.relationshipLabel != null ) { if ( !data.relationshipLabel.equals( data.previousRelationshipLabel ) ) { changedLabel = true; } } } return changedLabel; } private boolean determineDynamicLabels( String[] nodeLabelFields ) { for ( String nodeLabelField : nodeLabelFields ) { if ( StringUtils.isNotEmpty( nodeLabelField ) ) { return true; } } return false; } private void outputGraphValue( RowMetaInterface rowMeta, Object[] row ) throws KettleException { try { GraphData graphData = new GraphData(); graphData.setSourceTransformationName( getTransMeta().getName() ); graphData.setSourceStepName( getStepMeta().getName() ); GraphNodeData sourceNodeData = null; GraphNodeData targetNodeData = null; GraphRelationshipData relationshipData; if ( meta.getFromNodeProps().length > 0 ) { sourceNodeData = createGraphNodeData( rowMeta, row, meta.getFromNodeLabels(), data.fromLabelValues, data.fromNodeLabelIndexes, data.fromNodePropIndexes, meta.getFromNodePropNames(), meta.getFromNodePropPrimary(), "from" ); if ( !meta.isOnlyCreatingRelationships() ) { graphData.getNodes().add( sourceNodeData ); } } if ( meta.getToNodeProps().length > 0 ) { targetNodeData = createGraphNodeData( rowMeta, row, meta.getToNodeLabels(), data.toLabelValues, data.toNodeLabelIndexes, data.toNodePropIndexes, meta.getToNodePropNames(), meta.getToNodePropPrimary(), "to" ); if ( !meta.isOnlyCreatingRelationships() ) { graphData.getNodes().add( targetNodeData ); } } String relationshipLabel = null; if ( data.relationshipIndex >= 0 ) { relationshipLabel = getInputRowMeta().getString( row, data.relationshipIndex ); } if ( StringUtil.isEmpty( relationshipLabel ) && StringUtils.isNotEmpty( data.relationshipLabelValue ) ) { relationshipLabel = data.relationshipLabelValue; } if ( sourceNodeData != null && targetNodeData != null && StringUtils.isNotEmpty( relationshipLabel ) ) { relationshipData = new GraphRelationshipData(); relationshipData.setSourceNodeId( sourceNodeData.getId() ); relationshipData.setTargetNodeId( targetNodeData.getId() ); relationshipData.setLabel( relationshipLabel ); relationshipData.setId( sourceNodeData.getId() + " -> " + targetNodeData.getId() ); relationshipData.setPropertySetId( "relationship" ); // Add relationship properties... // // Set the properties // for ( int i = 0; i < data.relPropIndexes.length; i++ ) { ValueMetaInterface valueMeta = rowMeta.getValueMeta( data.relPropIndexes[ i ] ); Object valueData = row[ data.relPropIndexes[ i ] ]; String propertyName = meta.getRelPropNames()[ i ]; GraphPropertyDataType propertyType = GraphPropertyDataType.getTypeFromKettle( valueMeta ); Object propertyNeoValue = propertyType.convertFromKettle( valueMeta, valueData ); boolean propertyPrimary = false; relationshipData.getProperties().add( new GraphPropertyData( propertyName, propertyNeoValue, propertyType, propertyPrimary ) ); } graphData.getRelationships().add( relationshipData ); } // Pass it forward... // Object[] outputRowData = RowDataUtil.createResizedCopy( row, data.outputRowMeta.size() ); int startIndex = rowMeta.size(); outputRowData[ rowMeta.size() ] = graphData; putRow( data.outputRowMeta, outputRowData ); } catch ( Exception e ) { throw new KettleException( "Unable to calculate graph output value", e ); } } private GraphNodeData createGraphNodeData( RowMetaInterface rowMeta, Object[] row, String[] nodeLabels, String[] nodeLabelValues, int[] nodeLabelIndexes, int[] nodePropIndexes, String[] nodePropNames, boolean[] nodePropPrimary, String propertySetId ) throws KettleException { GraphNodeData nodeData = new GraphNodeData(); // The property set ID is simply either "Source" or "Target" // nodeData.setPropertySetId( propertySetId ); // Set the label(s) // List<String> labels = getNodeLabels( nodeLabels, nodeLabelValues, rowMeta, row, nodeLabelIndexes ); for ( String label : labels ) { nodeData.getLabels().add( label ); } StringBuilder nodeId = new StringBuilder(); // Set the properties // for ( int i = 0; i < nodePropIndexes.length; i++ ) { ValueMetaInterface valueMeta = rowMeta.getValueMeta( nodePropIndexes[ i ] ); Object valueData = row[ nodePropIndexes[ i ] ]; String propertyName = nodePropNames[ i ]; GraphPropertyDataType propertyType = GraphPropertyDataType.getTypeFromKettle( valueMeta ); Object propertyNeoValue = propertyType.convertFromKettle( valueMeta, valueData ); boolean propertyPrimary = nodePropPrimary[ i ]; nodeData.getProperties().add( new GraphPropertyData( propertyName, propertyNeoValue, propertyType, propertyPrimary ) ); // Part of the key... if ( nodePropPrimary[ i ] ) { if ( nodeId.length() > 0 ) { nodeId.append( "-" ); } nodeId.append( valueMeta.getString( valueData ) ); } } if ( nodeId.length() > 0 ) { nodeData.setId( nodeId.toString() ); } return nodeData; } public boolean init( StepMetaInterface smi, StepDataInterface sdi ) { meta = (Neo4JOutputMeta) smi; data = (Neo4JOutputData) sdi; if ( !meta.isReturningGraph() ) { // Connect to Neo4j using info metastore Neo4j Connection metadata // if ( StringUtils.isEmpty( meta.getConnection() ) ) { log.logError( "You need to specify a Neo4j connection to use in this step" ); return false; } try { // To correct lazy programmers who built certain PDI steps... // data.metaStore = MetaStoreUtil.findMetaStore( this ); data.neoConnection = NeoConnectionUtils.getConnectionFactory( data.metaStore ).loadElement( meta.getConnection() ); if (data.neoConnection==null) { log.logError("Connection '"+meta.getConnection()+"' could not be found in the metastore "+MetaStoreUtil.getMetaStoreDescription(metaStore)); return false; } data.neoConnection.initializeVariablesFrom( this ); data.version4 = data.neoConnection.isVersion4(); } catch ( MetaStoreException e ) { log.logError( "Could not gencsv Neo4j connection '" + meta.getConnection() + "' from the metastore", e ); return false; } data.batchSize = Const.toLong( environmentSubstitute( meta.getBatchSize() ), 1 ); } return super.init( smi, sdi ); } public void dispose( StepMetaInterface smi, StepDataInterface sdi ) { data = (Neo4JOutputData) sdi; if ( !isStopped() ) { try { wrapUpTransaction(); } catch ( KettleException e ) { logError( "Error wrapping up transaction", e ); setErrors( 1L ); stopAll(); } } if ( data.session != null ) { data.session.close(); } super.dispose( smi, sdi ); } private String getLabels( String nodeAlias, List<String> nodeLabels ) { if ( nodeLabels.isEmpty() ) { return null; } StringBuilder labels = new StringBuilder( nodeAlias ); for ( String nodeLabel : nodeLabels ) { labels.append( ":" ); labels.append( escapeLabel( nodeLabel ) ); } return labels.toString(); } private void processSummary( Result result ) throws KettleException { boolean error = false; ResultSummary summary = result.consume(); for ( Notification notification : summary.notifications() ) { log.logError( notification.title() + " (" + notification.severity() + ")" ); log.logError( notification.code() + " : " + notification.description() + ", position " + notification.position() ); error = true; } if ( error ) { throw new KettleException( "Error found while executing cypher statement(s)" ); } } private String buildParameterClause( String parameterName ) { if ( data.version4 ) { return "$" + parameterName; } else { return "{" + parameterName + "}"; } } private String generateMatchClause( String alias, String mapName, List<String> nodeLabels, String[] nodeProps, String[] nodePropNames, GraphPropertyType[] nodePropTypes, boolean[] nodePropPrimary, RowMetaInterface rowMeta, Object[] rowData, int[] nodePropIndexes, Map<String, Object> parameters, AtomicInteger paramNr ) throws KettleValueException { String matchClause = "(" + alias; for ( int i = 0; i < nodeLabels.size(); i++ ) { String label = escapeProp( nodeLabels.get( i ) ); matchClause += ":" + label; } matchClause += " {"; boolean firstProperty = true; for ( int i = 0; i < nodeProps.length; i++ ) { if ( nodePropPrimary[ i ] ) { if ( firstProperty ) { firstProperty = false; } else { matchClause += ", "; } String propName; if ( StringUtils.isNotEmpty( nodePropNames[ i ] ) ) { propName = nodePropNames[ i ]; } else { propName = nodeProps[ i ]; } String parameterName = "param" + paramNr.incrementAndGet(); if ( mapName == null ) { matchClause += propName + " : " + buildParameterClause( parameterName ); } else { matchClause += propName + " : " + mapName + "." + parameterName; } if ( parameters != null ) { ValueMetaInterface valueMeta = rowMeta.getValueMeta( nodePropIndexes[ i ] ); Object valueData = rowData[ nodePropIndexes[ i ] ]; GraphPropertyType propertyType = nodePropTypes[ i ]; Object neoValue = propertyType.convertFromKettle( valueMeta, valueData ); parameters.put( parameterName, neoValue ); } } } matchClause += " })"; return matchClause; } public List<String> getNodeLabels( String[] labelFields, String[] labelValues, RowMetaInterface rowMeta, Object[] rowData, int[] labelIndexes ) throws KettleValueException { List<String> labels = new ArrayList<>(); for ( int a = 0; a < labelFields.length; a++ ) { String label = null; if ( StringUtils.isNotEmpty( labelFields[ a ] ) ) { label = rowMeta.getString( rowData, labelIndexes[ a ] ); } if ( StringUtils.isEmpty( label ) && StringUtils.isNotEmpty( labelValues[ a ] ) ) { label = labelValues[ a ]; } if ( StringUtils.isNotEmpty( label ) ) { labels.add( label ); } } return labels; } public String escapeLabel( String str ) { if ( str.contains( " " ) || str.contains( "." ) ) { str = "`" + str + "`"; } return str; } public String escapeProp( String str ) { return StringEscapeUtils.escapeJava( str ); } private void createNodePropertyIndexes( Neo4JOutputMeta meta, Neo4JOutputData data, RowMetaInterface rowMeta, Object[] rowData ) throws KettleException { // Only create indexes on the first copy // if ( getCopy() != 0 ) { return; } createIndexForNode( data, meta.getFromNodeLabels(), meta.getFromNodeLabelValues(), meta.getFromNodeProps(), meta.getFromNodePropNames(), meta.getFromNodePropPrimary(), rowMeta, rowData ); createIndexForNode( data, meta.getToNodeLabels(), meta.getToNodeLabelValues(), meta.getToNodeProps(), meta.getToNodePropNames(), meta.getToNodePropPrimary(), rowMeta, rowData ); } private void createIndexForNode( Neo4JOutputData data, String[] nodeLabelFields, String[] nodeLabelValues, String[] nodeProps, String[] nodePropNames, boolean[] nodePropPrimary, RowMetaInterface rowMeta, Object[] rowData ) throws KettleValueException { // Which labels to index? // Set<String> labels = new HashSet<>(); labels.addAll( Arrays.asList( nodeLabelValues ).stream().filter( s -> StringUtils.isNotEmpty( s ) ).collect( Collectors.toList() ) ); for ( String nodeLabelField : nodeLabelFields ) { if ( StringUtils.isNotEmpty( nodeLabelField ) ) { String label = rowMeta.getString( rowData, nodeLabelField, null ); if ( StringUtils.isNotEmpty( label ) ) { labels.add( label ); } } } // Create a index on the primary fields of the node properties // for ( String label : labels ) { List<String> primaryProperties = new ArrayList<>(); for ( int f = 0; f < nodeProps.length; f++ ) { if ( nodePropPrimary[ f ] ) { if ( StringUtils.isNotEmpty( nodePropNames[ f ] ) ) { primaryProperties.add( nodePropNames[ f ] ); } else { primaryProperties.add( nodeProps[ f ] ); } } } if ( label != null && primaryProperties.size() > 0 ) { NeoConnectionUtils.createNodeIndex( log, data.session, Collections.singletonList( label ), primaryProperties ); } } } @Override public void batchComplete() throws KettleException { wrapUpTransaction(); } private void wrapUpTransaction() throws KettleException { if ( !isStopped() ) { if ( data.unwindList != null && data.unwindList.size() > 0 ) { emptyUnwindList( true ); // force write! } } // Allow gc // data.unwindList = new ArrayList<>(); } /** * Update the usagemap. Add all the labels to the node usage. * * @param labels * @param usage */ protected void updateUsageMap( List<String> labels, GraphUsage usage ) throws KettleValueException { if ( labels == null ) { return; } Map<String, Set<String>> stepsMap = data.usageMap.get( usage.name() ); if ( stepsMap == null ) { stepsMap = new HashMap<>(); data.usageMap.put( usage.name(), stepsMap ); } Set<String> labelSet = stepsMap.get( getStepname() ); if ( labelSet == null ) { labelSet = new HashSet<>(); stepsMap.put( getStepname(), labelSet ); } for ( String label : labels ) { if ( StringUtils.isNotEmpty( label ) ) { labelSet.add( label ); } } } }