diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java
index 0b8dc218..f6f308bd 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java
@@ -258,11 +258,30 @@
*
*
* '#' Wildcard
- * Valid only on the RHS of the spec, nested in an array, like "[#2]"
- * This wildcard is useful if you want to take a JSON map and turn it into a JSON array, and you do not care about the order of the array.
+ * Valid both on the LHS and RHS, but has different behavior / format on either side.
+ * They way to think of it, is that it allows you to specify a "synthentic" value, aka a value not found in the input data.
*
- * While Shiftr is doing its parallel tree walk of the input data and the spec, it tracks how many matched it has processed at each level
- * of the spec tree.
+ * On the RHS of the spec, # is only valid in the the context of an array, like "[#2]".
+ * What "[#2]" means is, go up the three 2 levels and ask that node how many matches it has had, and then use that as an index
+ * in the arrays.
+ * This means that, while Shiftr is doing its parallel tree walk of the input data and the spec, it tracks how many matches it
+ * has processed at each level of the spec tree.
+ *
+ * This useful if you want to take a JSON map and turn it into a JSON array, and you do not care about the order of the array.
+ *
+ * On the LHS of the spec, # allows you to specify a hard coded String to be place as a value in the output.
+ *
+ * The initial use-case for this feature was to be able to process a Boolean input value, and if the value is
+ * boolean true write out the string "enabled". Note, this was possible before, but it required two Shiftr steps.
+ *
+ *
+ * Example
+ * "hidden" : {
+ * "true" : { // if the value of "hidden" is true
+ * "#disabled" : "clients.clientId" // write the word "disabled" to the path "clients.clientId"
+ * }
+ * }
+ *
*
*
* '|' Wildcard
@@ -278,7 +297,10 @@
*
*
* '@' Wildcard
- * Valid only on the LHS of the spec.
+ * Valid only on both sides of the spec.
+ *
+ * The basic '@' on the LHS.
+ *
* This wildcard is necessary if you want to do put both the input value and the input key somewhere in the output JSON.
*
* Example '@' wildcard usage :
@@ -298,6 +320,13 @@
*
* Thus the '@' wildcard is the mean "copy the value of the data at this level in the tree, to the output".
*
+ * Advanced '@' sign wildcard.
+ * The format is lools like "@(3,title)", where
+ * "3" means go up the tree 3 levels and then lookup the key
+ * "title" and use the value at that key.
+ *
+ * See the filter*.json and transpose*.json Unit Test fixtures.
+ *
*
* JSON Arrays :
*
@@ -471,7 +500,7 @@ public Object transform( Object input ) {
// Create a root LiteralPathElement so that # is useful at the root level
LiteralPathElement rootLpe = new LiteralPathElement( ROOT_KEY );
WalkedPath walkedPath = new WalkedPath();
- walkedPath.add( rootLpe );
+ walkedPath.add( input, rootLpe );
rootSpec.apply( ROOT_KEY, input, walkedPath, output );
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityCompositeSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityCompositeSpec.java
index 2ed95a97..e0748cfc 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityCompositeSpec.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityCompositeSpec.java
@@ -144,7 +144,7 @@ public boolean apply( String inputKey, Object input, WalkedPath walkedPath, Obje
return false;
}
- walkedPath.add( thisLevel );
+ walkedPath.add( input, thisLevel );
// The specialChild can change the data object that I point to.
// Aka, my key had a value that was a List, and that gets changed so that my key points to a ONE value
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityLeafSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityLeafSpec.java
index 1026453f..654b2ab6 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityLeafSpec.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityLeafSpec.java
@@ -87,7 +87,7 @@ public Object applyToParentContainer ( String inputKey, Object input, WalkedPath
private Object performCardinalityAdjustment( String inputKey, Object input, WalkedPath walkedPath, Map parentContainer, LiteralPathElement thisLevel ) {
// Add our the LiteralPathElement for this level, so that write path References can use it as &(0,0)
- walkedPath.add( thisLevel );
+ walkedPath.add( input, thisLevel );
Object returnValue = null;
if ( cardinalityRelationship == CardinalityRelationship.MANY ) {
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathStep.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathStep.java
new file mode 100644
index 00000000..63307334
--- /dev/null
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathStep.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2014 Bazaarvoice, Inc.
+ *
+ * Licensed 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 com.bazaarvoice.jolt.common;
+
+import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement;
+
+/**
+ * A tuple class that contains the data for one level of a
+ * tree walk, aka a reference to the input for that level, and
+ * the LiteralPathElement that was matched at that level.
+ */
+public final class PathStep {
+ private final Object treeRef;
+ private final LiteralPathElement literalPathElement;
+
+ public PathStep(Object treeRef, LiteralPathElement literalPathElement) {
+ this.treeRef = treeRef;
+ this.literalPathElement = literalPathElement;
+ }
+
+ public Object getTreeRef() {
+ return treeRef;
+ }
+
+ public LiteralPathElement getLiteralPathElement() {
+ return literalPathElement;
+ }
+}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/WalkedPath.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/WalkedPath.java
index 06484d08..085276e1 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/WalkedPath.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/WalkedPath.java
@@ -23,6 +23,10 @@
/**
* DataStructure used by a SpecTransform during it's parallel tree walk.
*
+ * Basically this is Stack that records the steps down the tree that have been taken.
+ * For each level, there is a PathStep, which contains a pointer the data of that level,
+ * and a pointer to the LiteralPathElement matched at that level.
+ *
* At any given point in time, it represents where in the tree walk a Spec is operating.
* It is primarily used to by the ShiftrLeafSpec and CardinalityLeafSpec as a reference
* to lookup real values for output "&(1,1)" references.
@@ -30,32 +34,43 @@
* It is expected that as the SpecTransform navigates down the tree, LiteralElements will be added and then
* removed when that subtree has been walked.
*/
-public class WalkedPath extends ArrayList {
+public class WalkedPath extends ArrayList {
public WalkedPath() {
super();
}
- public WalkedPath( Collection c ) {
- super( c );
+ public WalkedPath(Collection c) {
+ super(c);
}
- public LiteralPathElement removeLast() {
- return remove( size() - 1 );
+ public WalkedPath( Object treeRef, LiteralPathElement literalPathElement ) {
+ super();
+ this.add( new PathStep( treeRef, literalPathElement ) );
+ }
+
+ /**
+ * Convenience method
+ */
+ public boolean add( Object treeRef, LiteralPathElement literalPathElement ) {
+ return super.add( new PathStep( treeRef, literalPathElement ) );
+ }
+
+ public void removeLast() {
+ remove(size() - 1);
}
/**
* Method useful to "&", "&1", "&2", etc evaluation.
*/
- public LiteralPathElement elementFromEnd( int idxFromEnd ) {
- if ( isEmpty() ) {
+ public PathStep elementFromEnd(int idxFromEnd) {
+ if (isEmpty()) {
return null;
}
- return get( size() - 1 - idxFromEnd );
+ return get(size() - 1 - idxFromEnd);
}
- public LiteralPathElement lastElement() {
- return get( size() - 1 );
+ public PathStep lastElement() {
+ return get(size() - 1);
}
-
}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AmpPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AmpPathElement.java
index 7897e1c4..e006ee1f 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AmpPathElement.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AmpPathElement.java
@@ -114,7 +114,7 @@ public String evaluate( WalkedPath walkedPath ) {
}
else {
AmpReference ref = (AmpReference) token;
- LiteralPathElement literalPathElement = walkedPath.elementFromEnd( ref.getPathIndex() );
+ LiteralPathElement literalPathElement = walkedPath.elementFromEnd( ref.getPathIndex() ).getLiteralPathElement();
String value = literalPathElement.getSubKeyRef( ref.getKeyGroup() );
output.append( value );
}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/ArrayPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/ArrayPathElement.java
index 96ce2052..74cdb2ad 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/ArrayPathElement.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/ArrayPathElement.java
@@ -98,12 +98,12 @@ public String evaluate( WalkedPath walkedPath ) {
return arrayIndex;
case HASH:
- LiteralPathElement element = walkedPath.elementFromEnd( ref.getPathIndex() );
+ LiteralPathElement element = walkedPath.elementFromEnd( ref.getPathIndex() ).getLiteralPathElement();
Integer index = element.getHashCount();
return index.toString();
case REFERENCE:
- LiteralPathElement lpe = walkedPath.elementFromEnd( ref.getPathIndex() );
+ LiteralPathElement lpe = walkedPath.elementFromEnd( ref.getPathIndex() ).getLiteralPathElement();
String keyPart;
if ( ref instanceof PathAndGroupReference ) {
@@ -118,7 +118,7 @@ public String evaluate( WalkedPath walkedPath ) {
return keyPart;
}
catch ( NumberFormatException nfe ) {
- throw new RuntimeException( " Evaluating canonical ReferencePathElement:" + this.getCanonicalForm() + ", and got a non integer result for reference:" + ref.getCanonicalForm() );
+ throw new RuntimeException( " Evaluating canonical ReferencePathElement:" + this.getCanonicalForm() + ", and got a non integer result:(" + keyPart + "), for reference:" + ref.getCanonicalForm() );
}
default:
throw new IllegalStateException( "ArrayPathType enum added two without updating this switch statement." );
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AtPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AtPathElement.java
index dd06c3d3..a7500c05 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AtPathElement.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AtPathElement.java
@@ -28,7 +28,7 @@ public AtPathElement( String key ) {
}
public LiteralPathElement match( String dataKey, WalkedPath walkedPath ) {
- return walkedPath.lastElement(); // copy what our parent was so that write keys of &0 and &1 both work.
+ return walkedPath.lastElement().getLiteralPathElement(); // copy what our parent was so that write keys of &0 and &1 both work.
}
@Override
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/DollarPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/DollarPathElement.java
index 17eff15c..da054671 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/DollarPathElement.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/DollarPathElement.java
@@ -35,7 +35,7 @@ public String getCanonicalForm() {
@Override
public String evaluate( WalkedPath walkedPath ) {
- LiteralPathElement pe = walkedPath.elementFromEnd( dRef.getPathIndex() );
+ LiteralPathElement pe = walkedPath.elementFromEnd( dRef.getPathIndex() ).getLiteralPathElement();
return pe.getSubKeyRef( dRef.getKeyGroup() );
}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/HashPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/HashPathElement.java
new file mode 100644
index 00000000..a87a2f8f
--- /dev/null
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/HashPathElement.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2013 Bazaarvoice, Inc.
+ *
+ * Licensed 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 com.bazaarvoice.jolt.common.pathelement;
+
+import com.bazaarvoice.jolt.common.WalkedPath;
+import com.bazaarvoice.jolt.exception.SpecException;
+import com.bazaarvoice.jolt.utils.StringTools;
+
+/**
+ * For use on the LHS, allows the user to specify an explicit string to write out.
+ * Aka given a input that is boolean, would want to write something out other than "true" / "false".
+ */
+public class HashPathElement extends BasePathElement implements MatchablePathElement {
+
+ private final String keyValue;
+
+ public HashPathElement( String key ) {
+ super(key);
+
+ if ( StringTools.isBlank( key ) ) {
+ throw new SpecException( "HashPathElement cannot have empty String as input." );
+ }
+
+ if ( ! key.startsWith( "#" ) ) {
+ throw new SpecException( "LHS # should start with a # : " + key );
+ }
+
+ if ( key.length() <= 1 ) {
+ throw new SpecException( "HashPathElement input is too short : " + key );
+ }
+
+
+ if ( key.charAt( 1 ) == '(' ) {
+ if ( key.charAt( key.length() -1 ) == ')' ) {
+ keyValue = key.substring( 2, key.length() -1 );
+ }
+ else {
+ throw new SpecException( "HashPathElement, mismatched parens : " + key );
+ }
+ }
+ else {
+ keyValue = key.substring( 1 );
+ }
+ }
+
+ @Override
+ public String getCanonicalForm() {
+ return "#(" + keyValue + ")";
+ }
+
+ @Override
+ public LiteralPathElement match( String dataKey, WalkedPath walkedPath ) {
+ return new LiteralPathElement( keyValue );
+ }
+}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/LiteralPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/LiteralPathElement.java
index a9d9964e..e8aea9f5 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/LiteralPathElement.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/LiteralPathElement.java
@@ -56,7 +56,7 @@ public String evaluate( WalkedPath walkedPath ) {
}
@Override
- public com.bazaarvoice.jolt.common.pathelement.LiteralPathElement match( String dataKey, WalkedPath walkedPath ) {
+ public LiteralPathElement match( String dataKey, WalkedPath walkedPath ) {
return getRawKey().equals( dataKey ) ? this : null ;
}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/TransposePathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/TransposePathElement.java
new file mode 100644
index 00000000..c250fb33
--- /dev/null
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/TransposePathElement.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2014 Bazaarvoice, Inc.
+ *
+ * Licensed 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 com.bazaarvoice.jolt.common.pathelement;
+
+import com.bazaarvoice.jolt.common.PathStep;
+import com.bazaarvoice.jolt.common.WalkedPath;
+import com.bazaarvoice.jolt.exception.SpecException;
+import com.bazaarvoice.jolt.shiftr.TransposeReader;
+import com.bazaarvoice.jolt.utils.StringTools;
+
+/**
+ * This PathElement is used by Shiftr to Transpose data.
+ *
+ * It can be used on the Left and Right hand sides of the spec.
+ *
+ * Input
+ * {
+ * "author" : "Stephen Hawking",
+ * "book" : "A Brief History of Time"
+ * }
+ *
+ * Wanted
+ * {
+ * "Stephen Hawking" : "A Brief History of Time"
+ * }
+ *
+ * The first part of the process is to allow a CompositeShiftr node to look down the input JSON tree.
+ *
+ * Spec
+ * {
+ * "@author" : "@book"
+ * }
+ *
+ *
+ * Secondly, we can look up the tree, and come down a different path to locate data.
+ *
+ * For example of this see the following ShiftrUnit tests :
+ * LHS Lookup : json/shiftr/filterParents.json
+ * RHS Lookup : json/shiftr/transposeComplex6_rhs-complex-at.json
+ *
+ *
+ * CanonicalForm Expansion
+ * Sugar
+ * "@2 -> "@(2,)
+ * "@(2) -> "@(2,)
+ * "@author" -> "@(0,author)"
+ * "@(author)" -> "@(0,author)"
+ *
+ * Splenda
+ * "@(a.b)" -> "@(0,a.b)"
+ * "@(a.&2.c)" -> "@(0,a.&(2,0).c)"
+ */
+public class TransposePathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement {
+
+ private final int upLevel;
+ private final TransposeReader subPathReader;
+ private final String canonicalForm;
+
+ /**
+ * Parse a text value from a Spec, into a TransposePathElement.
+ *
+ * @param key rawKey from a Jolt Spec file
+ * @return a TransposePathElement
+ */
+ public static TransposePathElement parse( String key ) {
+
+ if ( key == null || key.length() < 2 ) {
+ throw new SpecException( "'Transpose Input' key '@', can not be null or of length 1. Offending key : " + key );
+ }
+ if ( '@' != key.charAt( 0 ) ) {
+ throw new SpecException( "'Transpose Input' key must start with an '@'. Offending key : " + key );
+ }
+
+ // Strip off the leading '@' as we don't need it anymore.
+ String meat = key.substring( 1 );
+
+ if ( meat.contains( "@" ) ) {
+ throw new SpecException( "@ pathElement can not contain a nested @." );
+ }
+ if ( meat.contains( "*" ) || meat.contains( "[]" ) ) {
+ throw new SpecException( "'Transpose Input' can not contain expansion wildcards (* and []). Offending key : " + key );
+ }
+
+ // Check to see if the key is wrapped by parens
+ if ( meat.startsWith( "(" ) ) {
+ if ( meat.endsWith( ")" ) ) {
+ meat = meat.substring( 1, meat.length() - 1 );
+ }
+ else {
+ throw new SpecException( "@ path element that starts with '(' must have a matching ')'. Offending key : " + key );
+ }
+ }
+
+ return innerParse( key, meat );
+ }
+
+ /**
+ * Parse the core of the TransposePathElement key, once basic errors have been checked and
+ * syntax has been handled.
+ *
+ * @param originalKey The original text for reference.
+ * @param meat The string to actually parse into a TransposePathElement
+ * @return TransposePathElement
+ */
+ private static TransposePathElement innerParse( String originalKey, String meat ) {
+
+ char first = meat.charAt( 0 );
+ if ( Character.isDigit( first ) ) {
+ // loop until we find a comma or end of string
+ StringBuilder sb = new StringBuilder().append( first );
+ for ( int index = 1; index < meat.length(); index++ ) {
+ char c = meat.charAt( index );
+
+ // when we find a / the first comma, stop looking for integers, and just assume the rest is a String path
+ if( ',' == c ) {
+
+ int upLevel;
+ try {
+ upLevel = Integer.valueOf( sb.toString() );
+ }
+ catch ( NumberFormatException nfe ) {
+ // I don't know how this exception would get thrown, as all the chars were checked by isDigit, but oh well
+ throw new SpecException( "@ path element with non/mixed numeric key is not valid, key=" + originalKey );
+ }
+
+ return new TransposePathElement( originalKey, upLevel, meat.substring( index + 1 ) );
+ }
+ else if ( Character.isDigit( c ) ) {
+ sb.append( c );
+ }
+ else {
+ throw new SpecException( "@ path element with non/mixed numeric key is not valid, key=" + originalKey );
+ }
+ }
+
+ // if we got out of the for loop, then the whole thing was a number.
+ return new TransposePathElement( originalKey, Integer.valueOf( sb.toString() ), null );
+ }
+ else {
+ return new TransposePathElement( originalKey, 0, meat );
+ }
+ }
+
+ /**
+ * Private constructor used after parsing is done.
+ *
+ * @param originalKey for reference
+ * @param upLevel How far up the tree to go
+ * @param subPath Where to go down the tree
+ */
+ private TransposePathElement( String originalKey, int upLevel, String subPath ) {
+ super(originalKey);
+ this.upLevel = upLevel;
+ if ( StringTools.isEmpty( subPath ) ) {
+ this.subPathReader = null;
+ canonicalForm = "@(" + upLevel + ",)";
+ }
+ else {
+ subPathReader = new TransposeReader(subPath);
+ canonicalForm = "@(" + upLevel + "," + subPathReader.getCanonicalForm() + ")";
+ }
+ }
+
+ /**
+ * This method is used when the TransposePathElement is used on the LFH as data.
+ *
+ * Aka, normal "evaluate" returns either a Number or a String.
+ *
+ * @param walkedPath WalkedPath to evaluate against
+ * @return The data specified by this TransposePathElement, or null if it can't find anything.
+ */
+ public Object objectEvaluate( WalkedPath walkedPath ) {
+ // Grap the data we need from however far up the tree we are supposed to go
+ PathStep pathStep = walkedPath.elementFromEnd( upLevel );
+
+ Object treeRef = pathStep.getTreeRef();
+
+ // Now walk down from that level using the subPathReader
+ if ( subPathReader == null ) {
+ return treeRef;
+ }
+ else {
+ Object data = subPathReader.read( treeRef, walkedPath );
+ return data;
+ }
+ }
+
+ @Override
+ public String evaluate( WalkedPath walkedPath ) {
+
+ Object dataFromTranspose = objectEvaluate( walkedPath );
+
+ // Coerce a number into a String
+ if ( dataFromTranspose instanceof Number ) {
+ // the idea here being we are looking for an array index value
+ int val = ((Number) dataFromTranspose).intValue();
+ return Integer.toString( val );
+ }
+
+ // Coerce a boolean into a String
+ if ( dataFromTranspose instanceof Boolean ) {
+ return Boolean.toString( (Boolean) dataFromTranspose );
+ }
+
+ if ( dataFromTranspose == null || ! ( dataFromTranspose instanceof String ) ) {
+
+ // If this output path has a TransposePathElement, and when we evaluate it
+ // it does not resolve to a String, then return null
+ return null;
+ }
+
+ return (String) dataFromTranspose;
+ }
+
+
+ public LiteralPathElement match( String dataKey, WalkedPath walkedPath ) {
+ return walkedPath.lastElement().getLiteralPathElement(); // copy what our parent was so that write keys of &0 and &1 both work.
+ }
+
+ @Override
+ public String getCanonicalForm() {
+ return canonicalForm;
+ }
+}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/PathEvaluatingTraversal.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/PathEvaluatingTraversal.java
new file mode 100644
index 00000000..7cfc8c85
--- /dev/null
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/PathEvaluatingTraversal.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2014 Bazaarvoice, Inc.
+ *
+ * Licensed 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 com.bazaarvoice.jolt.shiftr;
+
+import com.bazaarvoice.jolt.common.WalkedPath;
+import com.bazaarvoice.jolt.common.pathelement.EvaluatablePathElement;
+import com.bazaarvoice.jolt.common.pathelement.PathElement;
+import com.bazaarvoice.jolt.exception.SpecException;
+import com.bazaarvoice.jolt.shiftr.spec.ShiftrSpec;
+import com.bazaarvoice.jolt.traversr.Traversr;
+import com.bazaarvoice.jolt.utils.StringTools;
+
+import java.util.*;
+
+/**
+ * Combines a Traversr with the ability to evaluate References against a WalkedPath.
+ *
+ * Convenience class for path based off a single dot notation String,
+ * like "rating.&1(2).&.value".
+ *
+ * This processes the dot notation path into internal data structures, so
+ * that the String processing only happens once.
+ */
+public abstract class PathEvaluatingTraversal {
+
+ private final List elements;
+ private final Traversr traversr;
+
+ public PathEvaluatingTraversal( String dotNotation ) {
+
+ if ( dotNotation.contains("*") || dotNotation.contains("$")) {
+ throw new SpecException("DotNotation (write key) can not contain '*' or '$'.");
+ }
+
+ List paths;
+ Traversr trav;
+
+ if ( StringTools.isNotBlank( dotNotation ) ) {
+
+ // Compute the path elements.
+ paths = ShiftrSpec.parseDotNotationRHS( dotNotation );
+
+ // Use the canonical versions of the path elements to create the Traversr
+ List traversrPaths = new ArrayList( paths.size() );
+ for ( PathElement pe : paths ) {
+ traversrPaths.add( pe.getCanonicalForm() );
+ }
+ trav = createTraversr( traversrPaths );
+ }
+ else {
+ paths = Collections.emptyList();
+ trav = createTraversr( Arrays.asList( "" ) );
+ }
+
+ List evalPaths = new ArrayList( paths.size() );
+ for( PathElement pe : paths ) {
+ if ( ! ( pe instanceof EvaluatablePathElement ) ) {
+ throw new SpecException( "RHS key=" + pe.getRawKey() + " is not a valid RHS key." );
+ }
+
+ evalPaths.add( (EvaluatablePathElement) pe );
+ }
+
+ this.elements = Collections.unmodifiableList( evalPaths );
+ this.traversr = trav;
+ }
+
+ protected abstract Traversr createTraversr(List paths);
+
+ /**
+ * Use the supplied WalkedPath, in the evaluation of each of our PathElements to
+ * build a concrete output path. Then use that output path to write the given
+ * data to the output.
+ *
+ * @param data data to write
+ * @param output data structure we are going to write the data to
+ * @param walkedPath reference used to lookup reference values like "&1(2)"
+ */
+ public void write( Object data, Map output, WalkedPath walkedPath ) {
+ List evaledPaths = evaluate( walkedPath );
+ if ( evaledPaths != null ) {
+ traversr.set( output, evaledPaths, data );
+ }
+ }
+
+ public Object read( Object data, WalkedPath walkedPath ) {
+ List evaledPaths = evaluate( walkedPath );
+ if ( evaledPaths == null ) {
+ return null;
+ }
+ return traversr.get( data, evaledPaths );
+ }
+
+ /**
+ * Use the supplied WalkedPath, in the evaluation of each of our PathElements.
+ *
+ * If our PathElements contained a TransposePathElement, we may return null.
+ *
+ * @param walkedPath used to lookup/evaluate PathElement references values like "&1(2)"
+ * @return null or fully evaluated Strings, possibly with concrete array references like "photos.[3]"
+ */
+ // Visible for testing
+ List evaluate( WalkedPath walkedPath ) {
+
+ List strings = new ArrayList( elements.size() );
+ for ( EvaluatablePathElement pathElement : elements ) {
+
+ String evaledLeafOutput = pathElement.evaluate( walkedPath );
+ if ( evaledLeafOutput == null ) {
+ // If this output path contains a TransposePathElement, and when evaluated,
+ // return null, then bail
+ return null;
+ }
+ strings.add( evaledLeafOutput );
+ }
+
+ return strings;
+ }
+
+ public int size() {
+ return elements.size();
+ }
+
+ public PathElement get( int index ) {
+ return elements.get( index );
+ }
+
+ /**
+ * Testing method.
+ */
+ public String getCanonicalForm() {
+ StringBuilder buf = new StringBuilder();
+
+ for ( PathElement pe : elements ) {
+ buf.append( "." ).append( pe.getCanonicalForm() );
+ }
+
+ return buf.substring( 1 ); // strip the leading "."
+ }
+}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversr.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversr.java
index b548fc8f..aea0b7bc 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversr.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversr.java
@@ -21,12 +21,19 @@
import com.bazaarvoice.jolt.traversr.SimpleTraversr;
import com.bazaarvoice.jolt.traversr.traversal.TraversalStep;
+/**
+ * Traverser that does not overwrite data.
+ */
public class ShiftrTraversr extends SimpleTraversr {
public ShiftrTraversr( String humanPath ) {
super( humanPath );
}
+ public ShiftrTraversr( List paths ) {
+ super( paths );
+ }
+
/**
* Do a Shift style insert :
* 1) if there is no data "there", then just set it
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrWriter.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrWriter.java
index 4e725298..f1908747 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrWriter.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrWriter.java
@@ -15,112 +15,22 @@
*/
package com.bazaarvoice.jolt.shiftr;
-import com.bazaarvoice.jolt.common.WalkedPath;
-import com.bazaarvoice.jolt.exception.SpecException;
-import com.bazaarvoice.jolt.common.pathelement.EvaluatablePathElement;
-import com.bazaarvoice.jolt.common.pathelement.PathElement;
-import com.bazaarvoice.jolt.shiftr.spec.ShiftrSpec;
import com.bazaarvoice.jolt.traversr.Traversr;
-import com.bazaarvoice.jolt.utils.StringTools;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import java.util.Map;
/**
- * Convenience class for path based off a single dot notation String,
- * like "rating.&1(2).&.value".
- *
- * This processes the dot notation path into internal data structures, so
- * that the String processing only happens once.
+ * Shiftr Specific version of a PathEvaluatingTraversal, where we supply a
+ * ShiftrTraversr.
*/
-public class ShiftrWriter {
-
- private final List elements;
- private final Traversr traversr;
+public class ShiftrWriter extends PathEvaluatingTraversal {
public ShiftrWriter( String dotNotation ) {
-
- if ( dotNotation.contains("@") || dotNotation.contains("*") || dotNotation.contains("$")) {
- throw new SpecException("DotNotation (write key) can not contain '@', '*', or '$'.");
- }
-
- List paths;
- Traversr trav;
-
- if ( StringTools.isNotBlank(dotNotation) ) {
- String[] split = dotNotation.split( "\\." );
-
- paths = ShiftrSpec.parse( split );
- trav = new ShiftrTraversr( dotNotation );
- }
- else {
- paths = Collections.emptyList();
- trav = new ShiftrTraversr( "" );
- }
-
- List evalPaths = new ArrayList( paths.size() );
- for( PathElement pe : paths ) {
- if ( ! ( pe instanceof EvaluatablePathElement ) ) {
- throw new SpecException( "RHS key=" + pe.getRawKey() + " is not a valid RHS key." );
- }
-
- evalPaths.add( (EvaluatablePathElement) pe );
- }
-
- this.elements = Collections.unmodifiableList( evalPaths );
- this.traversr = trav;
- }
-
- /**
- * Use the supplied WalkedPath, in the evaluation of each of our PathElements to
- * build a concrete output path. Then use that output path to write the given
- * data to the output.
- *
- * @param data data to write
- * @param output data structure we are going to write the data to
- * @param walkedPath reference used to lookup reference values like "&1(2)"
- */
- public void write( Object data, Map output, WalkedPath walkedPath ) {
- traversr.set( output, evaluate( walkedPath ), data );
+ super( dotNotation );
}
- /**
- * Use the supplied WalkedPath, in the evaluation of each of our PathElements
- * @param walkedPath used to lookup/evaluate PathElement references values like "&1(2)"
- * @return fully evaluated Strings, possibly with concrete array references like "photos.[3]"
- */
- // Visible for testing
- List evaluate( WalkedPath walkedPath ) {
-
- List strings = new ArrayList(elements.size());
- for ( EvaluatablePathElement pathElement : elements ) {
- String evaledLeafOutput = pathElement.evaluate( walkedPath );
- strings.add( evaledLeafOutput );
- }
-
- return strings;
- }
-
- public int size() {
- return elements.size();
- }
-
- public PathElement get( int index ) {
- return elements.get( index );
- }
-
- /**
- * Testing method.
- */
- public String getCanonicalForm() {
- StringBuilder buf = new StringBuilder();
-
- for ( PathElement pe : elements ) {
- buf.append( "." ).append( pe.getCanonicalForm() );
- }
-
- return buf.substring( 1 ); // strip the leading "."
+ @Override
+ protected Traversr createTraversr( List paths ) {
+ return new ShiftrTraversr( paths );
}
}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/TransposeReader.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/TransposeReader.java
new file mode 100644
index 00000000..b9562882
--- /dev/null
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/TransposeReader.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2014 Bazaarvoice, Inc.
+ *
+ * Licensed 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 com.bazaarvoice.jolt.shiftr;
+
+import com.bazaarvoice.jolt.traversr.SimpleTraversr;
+import com.bazaarvoice.jolt.traversr.Traversr;
+
+import java.util.List;
+
+/**
+ * The TransposeReader uses a PathEvaluatingTraversal with a SimpleTraversr.
+ *
+ * This means that as it walks a path in a tree structure (PathEvaluatingTraversal),
+ * it uses the behavior of the SimpleTraversr for tree traversal operations like
+ * get, set, and final set.
+ */
+public class TransposeReader extends PathEvaluatingTraversal {
+
+ public TransposeReader( String dotNotation ) {
+ super( dotNotation );
+ }
+
+ @Override
+ protected Traversr createTraversr( List paths ) {
+ return new SimpleTraversr( paths );
+ }
+}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrCompositeSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrCompositeSpec.java
index 69a7e426..9a1b886d 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrCompositeSpec.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrCompositeSpec.java
@@ -82,8 +82,10 @@ public ShiftrCompositeSpec(String rawKey, Map spec ) {
literals.put( child.pathElement.getRawKey(), child );
}
// special is it is "@" or "$"
- else if ( child.pathElement instanceof AtPathElement || (
- child.pathElement instanceof DollarPathElement ) ) {
+ else if ( child.pathElement instanceof AtPathElement ||
+ child.pathElement instanceof HashPathElement ||
+ child.pathElement instanceof DollarPathElement ||
+ child.pathElement instanceof TransposePathElement ) {
special.add( child );
}
else { // star || (& with children)
@@ -167,7 +169,7 @@ public boolean apply( String inputKey, Object input, WalkedPath walkedPath, Map<
}
// add ourselves to the path, so that our children can reference us
- walkedPath.add( thisLevel );
+ walkedPath.add( input, thisLevel );
// Handle any special / key based children first, but don't have them block anything
for( ShiftrSpec subSpec : specialChildren ) {
@@ -181,7 +183,7 @@ public boolean apply( String inputKey, Object input, WalkedPath walkedPath, Map<
walkedPath.removeLast();
// we matched so increment the matchCount of our parent
- walkedPath.lastElement().incrementHashCount();
+ walkedPath.lastElement().getLiteralPathElement().incrementHashCount();
return true;
}
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrLeafSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrLeafSpec.java
index 1d6b419d..ca722efb 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrLeafSpec.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrLeafSpec.java
@@ -16,6 +16,8 @@
package com.bazaarvoice.jolt.shiftr.spec;
import com.bazaarvoice.jolt.Shiftr;
+import com.bazaarvoice.jolt.common.pathelement.HashPathElement;
+import com.bazaarvoice.jolt.common.pathelement.TransposePathElement;
import com.bazaarvoice.jolt.shiftr.ShiftrWriter;
import com.bazaarvoice.jolt.utils.StringTools;
import com.bazaarvoice.jolt.exception.SpecException;
@@ -68,7 +70,7 @@ else if ( rhs instanceof List ) {
private static ShiftrWriter parseOutputDotNotation( Object rawObj ) {
if ( ! ( rawObj instanceof String ) ) {
- throw new SpecException( "Invalid Shiftr spec RHS. Should be a string or array of Strings. Value in question : " + rawObj );
+ throw new SpecException( "Invalid Shiftr spec, RHS should be a String or array of Strings. Value in question : " + rawObj );
}
// Prepend "root" to each output path.
@@ -99,20 +101,29 @@ public boolean apply( String inputKey, Object input, WalkedPath walkedPath, Map<
}
Object data;
- boolean realChild;
+ boolean realChild = false; // by default don't block further Shiftr matches
- if ( this.pathElement instanceof DollarPathElement ) {
- DollarPathElement subRef = (DollarPathElement) this.pathElement;
+ if ( this.pathElement instanceof DollarPathElement ||
+ this.pathElement instanceof HashPathElement ) {
- // The data is the parent key, so evaluate against the parent's path
- data = subRef.evaluate( walkedPath );
- realChild = false; // don't block further Shiftr matches
+ // The data is already encoded in the thisLevel object created by the pathElement.match called above
+ data = thisLevel.getCanonicalForm();
}
else if ( this.pathElement instanceof AtPathElement ) {
// The data is our parent's data
data = input;
- realChild = false; // don't block further Shiftr matches
+ }
+ else if ( this.pathElement instanceof TransposePathElement ) {
+ // We try to walk down the tree to find the value / data we want
+ TransposePathElement tpe = (TransposePathElement) this.pathElement;
+
+ // Note the data found may not be a String, thus we have to call the special objectEvaluate
+ data = tpe.objectEvaluate( walkedPath );
+ if ( data == null ) {
+ // if we could not find the value we want looking down the tree, bail
+ return false;
+ }
}
else {
// the data is the input
@@ -122,7 +133,7 @@ else if ( this.pathElement instanceof AtPathElement ) {
}
// Add our the LiteralPathElement for this level, so that write path References can use it as &(0,0)
- walkedPath.add( thisLevel );
+ walkedPath.add( input, thisLevel );
// Write out the data
for ( ShiftrWriter outputPath : shiftrWriters ) {
@@ -133,7 +144,7 @@ else if ( this.pathElement instanceof AtPathElement ) {
if ( realChild ) {
// we were a "real" child, so increment the matchCount of our parent
- walkedPath.lastElement().incrementHashCount();
+ walkedPath.lastElement().getLiteralPathElement().incrementHashCount();
}
return realChild;
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrSpec.java
index f0f75478..3994c53a 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrSpec.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrSpec.java
@@ -21,8 +21,10 @@
import com.bazaarvoice.jolt.utils.StringTools;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
import java.util.List;
+import java.util.LinkedList;
import java.util.Map;
/**
@@ -50,7 +52,7 @@
* The tree structure of formed by the CompositeSpecs is what is used during Shiftr transforms
* to do the parallel tree walk with the input data tree.
*
- * During the parallel tree walk, a Path is maintained, and used when
+ * During the parallel tree walk a stack of data (a WalkedPath) is maintained, and used when
* a tree walk encounters an Outputting spec to evaluate the wildcards in the write DotNotationPath.
*/
public abstract class ShiftrSpec {
@@ -59,13 +61,9 @@ public abstract class ShiftrSpec {
protected final MatchablePathElement pathElement;
public ShiftrSpec(String rawJsonKey) {
- List pathElements = parse( rawJsonKey );
- if ( pathElements.size() != 1 ){
- throw new SpecException( "Shiftr invalid LHS:" + rawJsonKey + " can not contain '.'" );
- }
+ PathElement pe = parseSingleKeyLHS( rawJsonKey );
- PathElement pe = pathElements.get( 0 );
if ( ! ( pe instanceof MatchablePathElement ) ) {
throw new SpecException( "Spec LHS key=" + rawJsonKey + " is not a valid LHS key." );
}
@@ -73,17 +71,28 @@ public ShiftrSpec(String rawJsonKey) {
this.pathElement = (MatchablePathElement) pe;
}
- // once all the shiftr specific logic is extracted.
- public static List parse( String key ) {
-
-
- if ( key.contains("@") ) {
+ /**
+ * Visible for Testing.
+ *
+ * Inspects the key in a particular order to determine the correct sublass of
+ * PathElement to create.
+ *
+ * @param key String that should represent a single PathElement
+ * @return a concrete implementation of PathElement
+ */
+ public static PathElement parseSingleKeyLHS( String key ) {
- return Arrays.asList( new AtPathElement( key ) );
+ if ( "@".equals( key ) ) {
+ return new AtPathElement( key );
+ }
+ else if ( key.startsWith("@") || key.contains( "@(" ) ) {
+ return TransposePathElement.parse( key );
+ }
+ else if ( key.contains( "@" ) ) {
+ throw new SpecException( "Invalid key:" + key + " can not have an @ other than at the front." );
}
else if ( key.contains("$") ) {
-
- return Arrays.asList( new DollarPathElement( key ) );
+ return new DollarPathElement( key );
}
else if ( key.contains("[") ) {
@@ -91,71 +100,260 @@ else if ( key.contains("[") ) {
throw new SpecException( "Invalid key:" + key + " has too many [] references.");
}
- // is canonical array?
- if ( key.charAt( 0 ) == '[' && key.charAt( key.length() - 1 ) == ']') {
- return Arrays.asList( new ArrayPathElement( key ) );
- }
-
- // Split syntactic sugar of "photos[]" --> [ "photos", "[]" ]
- // or "bob-&(3,1)-smith[&0]" --> [ "bob-&(3,1)-smith", "[&(0,0)]" ]
-
- String canonicalKey = key.replace( "[", ".[" );
- String[] subkeys = canonicalKey.split( "\\." );
-
- List subElements = parse( subkeys ); // at this point each sub key should be a valid key, so just recall parse
-
- for ( int index = 0; index < subElements.size() - 1; index++ ) {
- PathElement v = subElements.get( index );
- if ( v instanceof ArrayPathElement ) {
- throw new SpecException( "Array [..] must be the last thing in the key, was:" + key );
- }
- }
-
- return subElements;
+ return new ArrayPathElement( key );
}
- else if ( key.contains("&") ) {
+ else if ( key.contains( "&" ) ) {
if ( key.contains("*") )
{
throw new SpecException("Can't mix * with & ) ");
}
- return Arrays.asList( new AmpPathElement( key ) );
+ return new AmpPathElement( key );
}
else if ( "*".equals( key ) ) {
- return Arrays.asList( new StarAllPathElement( key ) );
+ return new StarAllPathElement( key );
}
- else if (key.contains("*" ) ) {
+ else if ( key.contains("*" ) ) {
int numOfStars = StringTools.countMatches(key, "*");
if(numOfStars == 1){
- return Arrays.asList( new StarSinglePathElement( key ) );
+ return new StarSinglePathElement( key );
}
else if(numOfStars == 2){
- return Arrays.asList( new StarDoublePathElement( key ) );
+ return new StarDoublePathElement( key );
}
else {
- return Arrays.asList( new StarRegexPathElement( key ) );
+ return new StarRegexPathElement( key );
}
}
+ else if ( key.contains("#" ) ) {
+ return new HashPathElement( key );
+ }
else {
- return Arrays.asList( new LiteralPathElement( key ) );
+ return new LiteralPathElement( key );
+ }
+ }
+
+
+ /**
+ * Helper method to turn a String into an Iterator
+ */
+ private static Iterator stringIterator(final String string) {
+ // Ensure the error is found as soon as possible.
+ if (string == null)
+ throw new NullPointerException();
+
+ return new Iterator() {
+ private int index = 0;
+
+ public boolean hasNext() {
+ return index < string.length();
+ }
+
+ public Character next() {
+
+ // Throw NoSuchElementException as defined by the Iterator contract,
+ // not IndexOutOfBoundsException.
+ if (!hasNext())
+ throw new NoSuchElementException();
+ return string.charAt(index++);
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ /**
+ * Given a dotNotation style outputPath like "data[2].&(1,1)", this method fixes the syntactic sugar
+ * of "data[2]" --> "data.[2]"
+ *
+ * This makes all the rest of the String processing easier once we know that we can always
+ * split on the '.' character.
+ *
+ * @param dotNotaton Output path dot notation
+ * @return
+ */
+ // TODO Unit Test this
+ private static String fixLeadingBracketSugar( String dotNotaton ) {
+
+ if ( dotNotaton == null || dotNotaton.length() == 0 ) {
+ return "";
+ }
+
+ char prev = dotNotaton.charAt( 0 );
+ StringBuilder sb = new StringBuilder();
+ sb.append( prev );
+
+ for ( int index = 1; index < dotNotaton.length(); index++ ) {
+ char curr = dotNotaton.charAt( index );
+
+ if ( curr == '[' ) {
+ if ( prev == '@' || prev == '.' ) {
+ // no need to add an extra '.'
+ }
+ else {
+ sb.append( '.' );
+ }
+ }
+
+ sb.append( curr );
+ prev = curr;
+ }
+
+ return sb.toString();
+ }
+
+
+ /**
+ * Parse RHS Transpose @ logic.
+ * "@(a.b)" or
+ * "@a.b
+ *
+ * This method expects that the the '@' character has already been seen.
+ * Thus if ther are
+ *
+ * @param iter iterator to pull data from
+ * @param dotNotationRef the original dotNotation string used for error messages
+ */
+ // TODO Unit Test this
+ private static String parseAtPathElement( Iterator iter, String dotNotationRef ) {
+
+ if ( ! iter.hasNext() ) {
+ return "";
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ // Strategy here is to walk thru the string looking for matching parenthesis.
+ // '(' increments the count, while ')' decrements it
+ // If we ever get negative there is a problem.
+ boolean isParensAt = false;
+ int atParensCount = 0;
+
+ char c = iter.next();
+ if ( c == '(' ) {
+ isParensAt = true;
+ atParensCount++;
+ }
+ else if ( c == '.' ) {
+ throw new SpecException( "Unable to parse dotNotation, invalid TransposePathElement : " + dotNotationRef );
+ }
+
+ sb.append( c );
+
+ while( iter.hasNext() ) {
+ c = iter.next();
+ sb.append( c );
+
+ // Parsing "@(a.b.[&2])"
+ if ( isParensAt ) {
+ if ( c == '(' ) {
+ throw new SpecException( "Unable to parse dotNotation, too many open parens '(' : " + dotNotationRef );
+ }
+ else if ( c == ')' ) {
+ atParensCount--;
+ }
+
+ if ( atParensCount == 0 ) {
+ return sb.toString();
+ }
+ else if ( atParensCount < 0 ) {
+ throw new SpecException( "Unable to parse dotNotation, specifically the '@()' part : " + dotNotationRef );
+ }
+ }
+ // Parsing "@abc.def
+ else if ( c == '.' ) {
+ return sb.toString();
+ }
+ }
+
+ // if we got to the end of the String and we have mismatched parenthesis throw an exception.
+ if ( isParensAt && atParensCount != 0 ) {
+ throw new SpecException( "Invalid @() pathElement from : " + dotNotationRef );
+ }
+ // Parsing "@abc"
+ return sb.toString();
+ }
+
+ /**
+ * Method that recursively parses a dotNotation String based on an iterator.
+ *
+ * This method will call out to parseAtPathElement
+ *
+ * @param pathStrings List to store parsed Strings that each represent a PathElement
+ * @param iter the iterator to pull characters from
+ * @param dotNotationRef the original dotNotation string used for error messages
+ * @return
+ */
+ private static List parseDotNotation( List pathStrings, Iterator iter, String dotNotationRef ) {
+
+ if ( ! iter.hasNext() ) {
+ return pathStrings;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ char c;
+ while( iter.hasNext() ) {
+
+ c = iter.next();
+
+ if( c == '@' ) {
+ sb.append( '@' );
+ sb.append( parseAtPathElement( iter, dotNotationRef ) );
+ pathStrings.add( sb.toString() );
+ sb = new StringBuilder();
+ }
+ else {
+ if ( c == '.' ) {
+ if ( sb.length() != 0 ) {
+ pathStrings.add( sb.toString() );
+ }
+ return parseDotNotation( pathStrings, iter, dotNotationRef );
+ }
+
+ sb.append( c );
+ }
}
+
+ if ( sb.length() != 0 ) {
+ pathStrings.add( sb.toString() );
+ }
+ return pathStrings;
}
- public static List parse( String[] keys ) {
+ /**
+ * @param refDotNotation the original dotNotation string used for error messages
+ * @return List of PathElements based on the provided List keys
+ */
+ private static List parseList( List keys, String refDotNotation ) {
ArrayList paths = new ArrayList();
for( String key: keys ) {
- List subPaths = parse( key );
- for ( PathElement path : subPaths ) {
- paths.add( path );
+ PathElement path = parseSingleKeyLHS( key );
+ if ( path instanceof AtPathElement ) {
+ throw new SpecException( "'.@.' is not valid on the RHS: " + refDotNotation );
}
+ paths.add( path );
}
return paths;
}
+ /**
+ * Parse the dotNotation of the RHS.
+ */
+ public static List parseDotNotationRHS( String dotNotation ) {
+ String fixedNotation = fixLeadingBracketSugar( dotNotation );
+ List pathStrs = parseDotNotation( new LinkedList(), stringIterator( fixedNotation ), dotNotation );
+
+ return parseList( pathStrs, dotNotation );
+ }
+
+
/**
* This is the main recursive method of the Shiftr parallel "spec" and "input" tree walk.
*
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversr.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversr.java
index 5ceb9062..da90876b 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversr.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversr.java
@@ -17,6 +17,8 @@
import com.bazaarvoice.jolt.traversr.traversal.TraversalStep;
+import java.util.List;
+
/**
* Simple Traversr that
*
@@ -29,6 +31,10 @@ public SimpleTraversr( String humanPath ) {
super( humanPath );
}
+ public SimpleTraversr( List paths ) {
+ super( paths );
+ }
+
@Override
public Object handleFinalSet( TraversalStep traversalStep, Object tree, String key, Object data ) {
return traversalStep.overwriteSet( tree, key, data );
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/Traversr.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/Traversr.java
index 62b92898..f4e6f934 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/Traversr.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/Traversr.java
@@ -82,6 +82,19 @@ public Traversr ( String humanPath ) {
root = rooty;
}
+ /**
+ * Constructor where we provide a known good set of pathElement Strings in a list.
+ * Aka, no need to extract it from a "Human Readable" form.
+ */
+ public Traversr( List paths ) {
+ TraversalStep rooty = null;
+ for ( int index = paths.size() -1 ; index >= 0; index--) {
+ rooty = makePathElement( paths.get(index), rooty );
+ }
+ traversalLength = paths.size();
+ root = rooty;
+ }
+
private TraversalStep makePathElement(String path, TraversalStep child) {
if ( "[]".equals( path ) ) {
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/BaseTraversalStep.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/BaseTraversalStep.java
index 943926aa..dbece56e 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/BaseTraversalStep.java
+++ b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/BaseTraversalStep.java
@@ -73,7 +73,7 @@ public final Object traverse( Object tree, Operation op, Iterator keys,
// Do nothing
// Throw Exception
// "make" the type ok by overwriting with a new container object (got list, wanted map, so trash the list by overwriting with a new map)
- throw new TraversrException( "Type mismatch on parent." );
+ return null;
}
}
}
diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrTest.java
index b7e672cd..a8e64593 100644
--- a/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrTest.java
+++ b/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrTest.java
@@ -208,6 +208,11 @@ public void process_itBlowsUp_fromTransform(Object spec) {
public Object[][] getTestCaseNames() {
return new Object[][] {
{"firstSample", true},
+ {"wolfermann1", false},
+ {"wolfermann2", false},
+ {"wolfermann2", false},
+ {"andrewkcarter1", false},
+ {"andrewkcarter2", false},
};
}
diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/JoltTestUtil.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/JoltTestUtil.java
index 76950d26..8d0c2d9b 100644
--- a/jolt-core/src/test/java/com/bazaarvoice/jolt/JoltTestUtil.java
+++ b/jolt-core/src/test/java/com/bazaarvoice/jolt/JoltTestUtil.java
@@ -22,16 +22,30 @@
public class JoltTestUtil {
private static Diffy diffy = new Diffy();
+ private static Diffy arrayOrderObliviousDiffy = new ArrayOrderObliviousDiffy();
public static void runDiffy( String failureMessage, Object expected, Object actual ) throws IOException {
+ runDiffy( diffy, failureMessage, expected, actual );
+ }
+
+ public static void runDiffy( Object expected, Object actual ) throws IOException {
+ runDiffy( diffy, "Failed", expected, actual );
+ }
+
+ public static void runArrayOrderObliviousDiffy( String failureMessage, Object expected, Object actual ) throws IOException {
+ runDiffy( arrayOrderObliviousDiffy, failureMessage, expected, actual );
+ }
+
+ public static void runArrayOrderObliviousDiffy( Object expected, Object actual ) throws IOException {
+ runDiffy( arrayOrderObliviousDiffy, "Failed", expected, actual );
+ }
+
+
+ private static void runDiffy( Diffy diffy, String failureMessage, Object expected, Object actual ) throws IOException {
Diffy.Result result = diffy.diff( expected, actual );
if (!result.isEmpty()) {
AssertJUnit.fail( failureMessage + ".\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual));
}
}
-
- public static void runDiffy( Object expected, Object actual ) throws IOException {
- runDiffy( "Failed", expected, actual );
- }
}
diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/ShiftrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/ShiftrTest.java
index 7564267e..6609e336 100644
--- a/jolt-core/src/test/java/com/bazaarvoice/jolt/ShiftrTest.java
+++ b/jolt-core/src/test/java/com/bazaarvoice/jolt/ShiftrTest.java
@@ -32,7 +32,11 @@ public Object[][] getTestCaseUnits() {
{"bucketToPrefixSoup"},
{"declaredOutputArray"},
{"explicitArrayKey"},
+ {"filterParallelArrays"},
+ {"filterParents1"},
+ {"filterParents2"},
{"firstSample"},
+ {"hashDefault"},
{"identity"},
{"inputArrayToPrefix"},
{"invertMap"},
@@ -41,6 +45,9 @@ public Object[][] getTestCaseUnits() {
{"listKeys"},
{"mapToList"},
{"mapToList2"},
+ {"mergeParallelArrays1_and-transpose"},
+ {"mergeParallelArrays2_and-do-not-transpose"},
+ {"mergeParallelArrays3_and-filter"},
{"multiPlacement"},
{"objectToArray"},
{"passThru"},
@@ -50,9 +57,27 @@ public Object[][] getTestCaseUnits() {
{"queryMappingXform"},
{"singlePlacement"},
{"specialKeys"},
+ {"transposeArrayContents1"},
+ {"transposeArrayContents2"},
+ {"transposeComplex1"},
+ {"transposeComplex2"},
+ {"transposeComplex3_both-sides-multipart"},
+ {"transposeComplex4_lhs-multipart-rhs-sugar"},
+ {"transposeComplex5_at-logic-with-embedded-array-lookups"},
+ {"transposeComplex6_rhs-complex-at"},
+ {"transposeComplex7_coerce-int-string-conversion"},
+ {"transposeComplex8_coerce-boolean-string-conversion"},
+ {"transposeInverseMap1"},
+ {"transposeInverseMap2"},
+ {"transposeLHS1"},
+ {"transposeLHS2"},
+ {"transposeLHS3"},
+ {"transposeSimple1"},
+ {"transposeSimple2"},
+ {"transposeSimple3"},
{"wildcards"},
{"wildcardSelfAndRef"},
- {"wildcardsWithOr"},
+ {"wildcardsWithOr"}
};
}
diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/PathElementTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/PathElementTest.java
index b61b02d9..4a33a15b 100644
--- a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/PathElementTest.java
+++ b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/PathElementTest.java
@@ -27,7 +27,6 @@
import org.testng.AssertJUnit;
import org.testng.annotations.Test;
-import java.util.Arrays;
import java.util.List;
// Todo Now that the PathElement classes have been split out (no longer inner classes)
@@ -105,8 +104,8 @@ public void arrayRefTest() {
@Test
public void calculateOutputTest_refsOnly() {
- MatchablePathElement pe1 = (MatchablePathElement) ShiftrSpec.parse( "tuna-*-marlin-*" ).get( 0 );
- MatchablePathElement pe2 = (MatchablePathElement) ShiftrSpec.parse( "rating-*" ).get( 0 );
+ MatchablePathElement pe1 = (MatchablePathElement) ShiftrSpec.parseSingleKeyLHS( "tuna-*-marlin-*" );
+ MatchablePathElement pe2 = (MatchablePathElement) ShiftrSpec.parseSingleKeyLHS( "rating-*" );
LiteralPathElement lpe = pe1.match( "tuna-marlin", new WalkedPath() );
AssertJUnit.assertNull( lpe );
@@ -118,26 +117,28 @@ public void calculateOutputTest_refsOnly() {
AssertJUnit.assertEquals( "A" , lpe.getSubKeyRef( 1 ) );
AssertJUnit.assertEquals( "AAA" , lpe.getSubKeyRef( 2 ) );
- LiteralPathElement lpe2 = pe2.match( "rating-BBB", new WalkedPath( Arrays.asList( lpe ) ) );
+ LiteralPathElement lpe2 = pe2.match( "rating-BBB", new WalkedPath( null, lpe ) );
AssertJUnit.assertEquals( "rating-BBB", lpe2.getRawKey() );
AssertJUnit.assertEquals( "rating-BBB", lpe2.getSubKeyRef( 0 ) );
AssertJUnit.assertEquals( 2, lpe2.getSubKeyCount() );
AssertJUnit.assertEquals( "BBB" , lpe2.getSubKeyRef( 1 ) );
ShiftrWriter outputPath = new ShiftrWriter( "&(1,2).&.value" );
+ WalkedPath twoSteps = new WalkedPath( null, lpe );
+ twoSteps.add( null, lpe2 );
{
EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get( 0 );
- String evaledLeafOutput = outputElement.evaluate( new WalkedPath( Arrays.asList( lpe, lpe2 ) ) );
+ String evaledLeafOutput = outputElement.evaluate( twoSteps );
AssertJUnit.assertEquals( "AAA", evaledLeafOutput );
}
{
EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get( 1 );
- String evaledLeafOutput = outputElement.evaluate( new WalkedPath( Arrays.asList( lpe, lpe2 ) ) );
+ String evaledLeafOutput = outputElement.evaluate( twoSteps );
AssertJUnit.assertEquals( "rating-BBB", evaledLeafOutput );
}
{
EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get( 2 );
- String evaledLeafOutput = outputElement.evaluate( new WalkedPath( Arrays.asList( lpe, lpe2 ) ) );
+ String evaledLeafOutput = outputElement.evaluate( twoSteps );
AssertJUnit.assertEquals( "value", evaledLeafOutput );
}
}
@@ -146,15 +147,15 @@ public void calculateOutputTest_refsOnly() {
public void calculateOutputTest_arrayIndexes() {
// simulate Shiftr LHS specs
- MatchablePathElement pe1 = (MatchablePathElement) ShiftrSpec.parse( "tuna-*-marlin-*" ).get( 0 );
- MatchablePathElement pe2 = (MatchablePathElement) ShiftrSpec.parse( "rating-*" ).get( 0 );
+ MatchablePathElement pe1 = (MatchablePathElement) ShiftrSpec.parseSingleKeyLHS( "tuna-*-marlin-*" );
+ MatchablePathElement pe2 = (MatchablePathElement) ShiftrSpec.parseSingleKeyLHS( "rating-*" );
// match them against some data to get LiteralPathElements with captured values
LiteralPathElement lpe = pe1.match( "tuna-2-marlin-3", new WalkedPath() );
AssertJUnit.assertEquals( "2" , lpe.getSubKeyRef( 1 ) );
AssertJUnit.assertEquals( "3" , lpe.getSubKeyRef( 2 ) );
- LiteralPathElement lpe2 = pe2.match( "rating-BBB", new WalkedPath( Arrays.asList( lpe ) ) );
+ LiteralPathElement lpe2 = pe2.match( "rating-BBB", new WalkedPath( null, lpe ) );
AssertJUnit.assertEquals( 2, lpe2.getSubKeyCount() );
AssertJUnit.assertEquals( "BBB" , lpe2.getSubKeyRef( 1 ) );
@@ -165,8 +166,9 @@ public void calculateOutputTest_arrayIndexes() {
AssertJUnit.assertEquals( "tuna.[&(1,1)].marlin.[&(1,2)].&(0,1)", shiftrWriter.getCanonicalForm() );
// Evaluate the write path against the LiteralPath elements we build above ( like Shiftr does )
- WalkedPath walkedPath = new WalkedPath( Arrays.asList( lpe, lpe2 ) );
- List stringPath = shiftrWriter.evaluate( walkedPath );
+ WalkedPath twoSteps = new WalkedPath( null, lpe );
+ twoSteps.add( null, lpe2 );
+ List stringPath = shiftrWriter.evaluate( twoSteps );
AssertJUnit.assertEquals( "tuna", stringPath.get( 0 ) );
AssertJUnit.assertEquals( "2", stringPath.get( 1 ) );
diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrUnitTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrUnitTest.java
index 54a98719..eaeb3ece 100644
--- a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrUnitTest.java
+++ b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrUnitTest.java
@@ -18,12 +18,17 @@
import com.bazaarvoice.jolt.JoltTestUtil;
import com.bazaarvoice.jolt.JsonUtils;
import com.bazaarvoice.jolt.Shiftr;
+import com.bazaarvoice.jolt.common.pathelement.PathElement;
import com.bazaarvoice.jolt.exception.SpecException;
+import com.bazaarvoice.jolt.shiftr.spec.ShiftrSpec;
+import com.google.common.base.Joiner;
+import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
public class ShiftrUnitTest {
@@ -104,9 +109,13 @@ public Object[][] badSpecs() throws IOException {
JsonUtils.jsonToMap( "{ \"tuna-*-marlin-*\" : { \"rating-@\" : \"&(1,2).&.value\" } }" ),
},
{
- "RHS @",
+ "RHS @ by itself",
JsonUtils.jsonToMap( "{ \"tuna-*-marlin-*\" : { \"rating-*\" : \"&(1,2).@.value\" } }" ),
},
+ {
+ "RHS @ with bad Parens",
+ JsonUtils.jsonToMap( "{ \"tuna-*-marlin-*\" : { \"rating-*\" : \"&(1,2).@(data.&(1,1).value\" } }" ),
+ },
{
"RHS *",
JsonUtils.jsonToMap( "{ \"tuna-*-marlin-*\" : { \"rating-*\" : \"&(1,2).*.value\" } }" ),
@@ -130,4 +139,58 @@ public Object[][] badSpecs() throws IOException {
public void failureUnitTest(String testName, Object spec) {
new Shiftr( spec );
}
+
+ /**
+ * @return canonical dotNotation String built from the given paths
+ */
+ public String buildCanonicalString( List paths ) {
+
+ List pathStrs = new ArrayList( paths.size() );
+ for( PathElement pe : paths ) {
+ pathStrs.add( pe.getCanonicalForm() );
+ }
+
+ return Joiner.on(".").join( paths );
+ }
+
+
+ @DataProvider
+ public Object[][] validRHS() throws IOException {
+ return new Object[][]{
+ { "@a", "@(0,a)" },
+ { "@abc", "@(0,abc)" },
+ { "@a.b.c", "@(0,a).b.c" },
+ { "@a.b.@c", "@(0,a).b.@(0,c)" },
+ { "@(a[2].&).b.@c", "@(0,a.[2].&(0,0)).b.@(0,c)" },
+ { "a[&2].@b[1].c", "a.[&(2,0)].@(0,b).[1].c" }
+ };
+ }
+
+ @Test(dataProvider = "validRHS" )
+ public void validRHSTests( String dotNotation, String expected ) {
+ List paths = ShiftrSpec.parseDotNotationRHS( dotNotation );
+ String actualCanonicalForm = buildCanonicalString( paths );
+
+ Assert.assertEquals( actualCanonicalForm, expected, "TestCase: " + dotNotation );
+ }
+
+
+ @DataProvider
+ public Object[][] badRHS() throws IOException {
+ return new Object[][]{
+ { "@" },
+ { "a@" },
+ { "@a@b" },
+ { "@(a.b.&(2,2)" }, // missing trailing )
+ { "@(a.b.&(2,2).d" }, // missing trailing )
+ { "@(a.b.@c).d" },
+ { "@(a.*.c)" }, // @ can not contain a *
+ { "@(a.$2.c)" }, // @ can not contain a $
+ };
+ }
+
+ @Test(dataProvider = "badRHS", expectedExceptions = SpecException.class)
+ public void failureRHSTests( String dotNotation ) {
+ ShiftrSpec.parseDotNotationRHS( dotNotation );
+ }
}
diff --git a/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter1.json b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter1.json
new file mode 100644
index 00000000..f31d3d35
--- /dev/null
+++ b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter1.json
@@ -0,0 +1,53 @@
+{
+ // Test from Jolt Issue 98 : Question one from andrewkcarter
+ // Summary : Bucket data from an Array, based on a leaf level value
+
+ "input": {
+ "entities": [
+ {
+ "type": "alpha",
+ "data": "foo"
+ },
+ {
+ "type": "beta",
+ "data": "bar"
+ },
+ {
+ "type": "alpha",
+ "data": "baz"
+ }
+ ]
+ },
+
+ "spec": [
+ {
+ "operation": "shift",
+ "spec": {
+ "entities": {
+ // The "*" matches each Map pair of "type" and "data".
+ // We then write the Map pair, to the value of the "type" key, in a forced array
+ "*" : "@type[]"
+ }
+ }
+ }
+ ],
+
+ "expected": {
+ "alpha":[
+ {
+ "type":"alpha",
+ "data":"foo"
+ },
+ {
+ "type":"alpha",
+ "data":"baz"
+ }
+ ],
+ "beta":[
+ {
+ "type":"beta",
+ "data":"bar"
+ }
+ ]
+ }
+}
diff --git a/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter2.json b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter2.json
new file mode 100644
index 00000000..9a93425b
--- /dev/null
+++ b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter2.json
@@ -0,0 +1,66 @@
+{
+ // Test from Jolt Issue 98 : Question two from andrewkcarter
+ // Summary : Filter books by there availability type. In particular just want Paperback books.
+
+ "input": {
+ "books": [
+ {
+ "title": "foo",
+ "availability": [
+ "online"
+ ]
+ },
+ {
+ "title": "bar",
+ "availability": [
+ "online",
+ "paperback"
+ ]
+ },
+ {
+ "title": "baz",
+ "availability": [
+ "paperback"
+ ]
+ }
+ ]
+ },
+
+ "spec": [
+ {
+ "operation": "shift",
+ "spec": {
+
+ "books": {
+ "*": {
+ "availability": {
+ "*": {
+ "paperback": {
+ "@3": "PaperBooks[]"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ],
+
+ "expected": {
+ "PaperBooks":[
+ {
+ "title":"bar",
+ "availability":[
+ "online",
+ "paperback"
+ ]
+ },
+ {
+ "title":"baz",
+ "availability":[
+ "paperback"
+ ]
+ }
+ ]
+ }
+}
diff --git a/jolt-core/src/test/resources/json/chainr/integration/wolfermann1.json b/jolt-core/src/test/resources/json/chainr/integration/wolfermann1.json
new file mode 100644
index 00000000..8921ec9d
--- /dev/null
+++ b/jolt-core/src/test/resources/json/chainr/integration/wolfermann1.json
@@ -0,0 +1,54 @@
+{
+ // Test from Jolt Issue 104 : Question one from ThorstenWolfermann
+ // Summary : Take nested input, and "flatten" it
+
+ "input": {
+ "level1": {
+ "L1Attribute": "6643287c-4800-49dd-b5cb-e0cf3ea637a9",
+ "L1SecondAttribtue": 543,
+ "level2": {
+ "L2Attribute": "d1bc9284-3087-4a92-904a-42f041f4fc23",
+ "L2SecondAttribute": 1234,
+ "leveln": {
+ "L3Attribute": "d1bc9284-3087-4a92-904a-42f041f4fc23",
+ "L3SecondAttribute": 1234
+ }
+ }
+ }
+ },
+
+ "spec": [
+ {
+ "operation": "shift",
+ "spec": {
+ "level*": {
+ "*Attribute": "&1.&0",
+ "*SecondAttribtue": "&1.&0",
+ "level*": {
+ "*Attribute": "&1.&0",
+ "*SecondAttribute": "&1.&0",
+ "level*": {
+ "*Attribute": "&1.&0",
+ "*SecondAttribute": "&1.&0"
+ }
+ }
+ }
+ }
+ }
+ ],
+
+ "expected": {
+ "level1": {
+ "L1Attribute": "6643287c-4800-49dd-b5cb-e0cf3ea637a9",
+ "L1SecondAttribtue": 543
+ },
+ "level2": {
+ "L2Attribute": "d1bc9284-3087-4a92-904a-42f041f4fc23",
+ "L2SecondAttribute": 1234
+ },
+ "leveln": {
+ "L3Attribute": "d1bc9284-3087-4a92-904a-42f041f4fc23",
+ "L3SecondAttribute": 1234
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/chainr/integration/wolfermann2.json b/jolt-core/src/test/resources/json/chainr/integration/wolfermann2.json
new file mode 100644
index 00000000..687367e0
--- /dev/null
+++ b/jolt-core/src/test/resources/json/chainr/integration/wolfermann2.json
@@ -0,0 +1,88 @@
+{
+ // Test from Jolt Issue 104 : Question two from ThorstenWolfermann
+ // Summary : Take nested input with arrays, and "flatten" it
+ //
+ // The first pass gets all the data into a single array, but with extra stuff in each slot.
+ // The second pass extracts and passes thru the desired values in the array.
+ // The "[]" in the Right Hand Side spec means place the data into an auto expanding array.
+
+ "input": {
+ "environment": {
+ "value": 123,
+ "orderLevel": [
+ {
+ "rate": "low",
+ "value": 666
+ },
+ {
+ "rate": "low",
+ "start": 321,
+ "orderLevel-1": [
+ {
+ "rate1": "OL",
+ "value": 444
+ },
+ {
+ "rate": "OL3",
+ "start": 444
+ }
+ ]
+ }
+ ]
+ }
+ },
+
+ "spec": [
+ {
+ "operation": "shift",
+ "spec": {
+ "environment": {
+ "orderLevel": {
+ "*": {
+ "@": "Data[]",
+ "orderLevel-1": {
+ "*": "Data[]"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "operation": "shift",
+ "spec": {
+ "Data": {
+ "*": {
+ "rate": "Data2.Data[&1].rate",
+ "rate*": "Data2.Data[&1].&",
+ "value": "Data2.Data[&1].value",
+ "start": "Data2.Data[&1].start"
+ }
+ }
+ }
+ }
+ ],
+
+ "expected": {
+ "Data2": {
+ "Data": [
+ {
+ "rate": "low",
+ "value": 666
+ },
+ {
+ "rate": "low",
+ "start": 321
+ },
+ {
+ "rate1": "OL",
+ "value": 444
+ },
+ {
+ "rate": "OL3",
+ "start": 444
+ }
+ ]
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/filterParallelArrays.json b/jolt-core/src/test/resources/json/shiftr/filterParallelArrays.json
new file mode 100644
index 00000000..763f7821
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/filterParallelArrays.json
@@ -0,0 +1,24 @@
+{
+ "input": {
+ "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ],
+ "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ]
+ },
+
+ "spec": {
+ "states": {
+ "*": {
+ "Ar*": { // only match states that start with "Ar"
+ // for those states, grab the captital, and use that as the output value
+ "@(3,capitals[&1])": "capitals[]",
+ "$" : "states[]"
+ }
+ }
+ }
+ },
+
+
+ "expected": {
+ "states" : [ "Arizona", "Arkansas" ],
+ "capitals" : [ "Phoenix" , "Little Rock" ]
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/filterParents1.json b/jolt-core/src/test/resources/json/shiftr/filterParents1.json
new file mode 100644
index 00000000..de53d32b
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/filterParents1.json
@@ -0,0 +1,49 @@
+{
+ // Identify all the book titles that are available in Paperback.
+ "input": {
+ "books": [
+ {
+ "title": "JavaScript",
+ "availability": [
+ "online"
+ ]
+ },
+ {
+ "title": "Scala",
+ "availability": [
+ "online",
+ "paperback"
+ ]
+ },
+ {
+ "title": "Java",
+ "availability": [
+ "paperback"
+ ]
+ }
+ ]
+ },
+
+ "spec": {
+ "books": {
+ "*": {
+ "availability": {
+ "*": { // match all elements of the availability array
+ "paperback": { // if the word paperback exists match it
+ "@(3,title)": "PaperBacks" // Look up the tree 3 levels, then back down and grab the value for the "title"
+ // and write it to PaperBacks in the output
+ },
+ "online" : {
+ "@(3,title)": "Online"
+ }
+ }
+ }
+ }
+ }
+ },
+
+ "expected": {
+ "PaperBacks": [ "Scala", "Java" ],
+ "Online" : [ "JavaScript", "Scala" ]
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/filterParents2.json b/jolt-core/src/test/resources/json/shiftr/filterParents2.json
new file mode 100644
index 00000000..bbfeb241
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/filterParents2.json
@@ -0,0 +1,55 @@
+{
+ // Identify all the book titles that are available in Paperback.
+ "input": {
+ "books": [
+ {
+ "book": {
+ "title": "JavaScript",
+ "ISBN": "1234"
+ },
+ "availability": [
+ "online"
+ ]
+ },
+ {
+ "book": {
+ "title": "Scala",
+ "ISBN": "9876"
+ },
+ "availability": [
+ "online",
+ "paperback"
+ ]
+ },
+ {
+ "book": {
+ "title": "Java",
+ "ISBN": "5555"
+ },
+ "availability": [
+ "paperback"
+ ]
+ }
+ ]
+ },
+
+ "spec": {
+ "books": {
+ "*": {
+ "availability": {
+ "*": { // match all elements of the availability array
+ "*": { // match any availability type
+ "@(3,book.title)": "&[]" // Look up the tree 3 levels, then back down and grab the value for the "title"
+ // and write out to the top level
+ }
+ }
+ }
+ }
+ }
+ },
+
+ "expected": {
+ "paperback": [ "Scala", "Java" ],
+ "online": [ "JavaScript", "Scala" ]
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/hashDefault.json b/jolt-core/src/test/resources/json/shiftr/hashDefault.json
new file mode 100644
index 00000000..3341457c
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/hashDefault.json
@@ -0,0 +1,37 @@
+{
+ "input": {
+ "data" : {
+ "1234" : {
+ "clientId": "12",
+ "hidden" : true
+ },
+ "1235" : {
+ "clientId": "35",
+ "hidden" : false
+ }
+ }
+ },
+
+ "spec": {
+ "data" : {
+ "*" : {
+ "hidden" : {
+ "true" : { // if hidden is true, then write the value disabled to the RHS output path
+ // Also @(3,clientId) means go up 3 levels, to the "1234" or "1235" level, then lookup / down the tree for the value of "clientId"
+ "#disabled" : "clients.@(3,clientId)"
+ },
+ "false" : {
+ "#enabled" : "clients.@(3,clientId)"
+ }
+ }
+ }
+ }
+ },
+
+ "expected": {
+ "clients" : {
+ "12" : "disabled",
+ "35" : "enabled"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays1_and-transpose.json b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays1_and-transpose.json
new file mode 100644
index 00000000..05bf9d1d
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays1_and-transpose.json
@@ -0,0 +1,25 @@
+{
+ "input": {
+ "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ],
+ "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ]
+ },
+
+ "spec": { // Level 2 : the root
+ "capitals": { // Level 1 : capitals
+
+ // Write out the "value" of each array index, aka 0->Montgomery, so the "Montgomery" part
+ // to "states.Alabama" where "Alabama" is 2 levels up the tree,
+ // then down into the "states" array, indexing using the same index value we matched
+ "*": "states.@(2,states[&])"
+ }
+ },
+
+ "expected": {
+ "states" : {
+ "Alabama" : "Montgomery",
+ "Alaska" : "Juneau",
+ "Arizona" : "Phoenix",
+ "Arkansas" : "Little Rock"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays2_and-do-not-transpose.json b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays2_and-do-not-transpose.json
new file mode 100644
index 00000000..a3b27c28
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays2_and-do-not-transpose.json
@@ -0,0 +1,45 @@
+{
+ "input": {
+ "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ],
+ "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ]
+ },
+
+ "spec": {
+ "states": {
+ "*": {
+ "*": {
+ "$": "states[&2].state"
+ }
+ }
+ },
+ "capitals": {
+ "*": {
+ "*": {
+ "$": "states[&2].capital"
+ }
+ }
+ }
+ },
+
+
+ "expected": {
+ "states" : [
+ {
+ "state" : "Alabama",
+ "capital" : "Montgomery"
+ },
+ {
+ "state" : "Alaska",
+ "capital" : "Juneau"
+ },
+ {
+ "state" : "Arizona",
+ "capital" : "Phoenix"
+ },
+ {
+ "state" : "Arkansas",
+ "capital" : "Little Rock"
+ }
+ ]
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays3_and-filter.json b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays3_and-filter.json
new file mode 100644
index 00000000..f6ca9386
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays3_and-filter.json
@@ -0,0 +1,25 @@
+{
+ "input": {
+ "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ],
+ "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ]
+ },
+
+ "spec": {
+ "states": {
+ "*": {
+ "Ar*": { // only match states that start with "Ar"
+ // for those states, grab the captital, and use that as the output value
+ "@(3,capitals[&1])": "states.&1"
+ }
+ }
+ }
+ },
+
+
+ "expected": {
+ "states" : {
+ "Arizona" : "Phoenix",
+ "Arkansas" : "Little Rock"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeArrayContents1.json b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents1.json
new file mode 100644
index 00000000..0c74b3d8
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents1.json
@@ -0,0 +1,33 @@
+{
+ "input": {
+ "photos" : [
+ {
+ "id": "1234",
+ "fileName": "Acme.jpg"
+ },
+ {
+ "id": "9876",
+ "fileName": "Boxy.jpg"
+ }
+ ]
+ },
+
+ "spec": {
+ "photos" : {
+ "*" : {
+ "@fileName": "photos[&1].@id"
+ }
+ }
+ },
+
+ "expected": {
+ "photos" : [
+ {
+ "1234" : "Acme.jpg"
+ },
+ {
+ "9876" : "Boxy.jpg"
+ }
+ ]
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeArrayContents2.json b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents2.json
new file mode 100644
index 00000000..58a771bf
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents2.json
@@ -0,0 +1,29 @@
+{
+ "input": {
+ "photos" : [
+ {
+ "id": "1234",
+ "fileName": "Acme.jpg"
+ },
+ {
+ "id": "9876",
+ "fileName": "Boxy.jpg"
+ }
+ ]
+ },
+
+ "spec": {
+ "photos" : {
+ "*" : {
+ "@fileName": "photos.@id"
+ }
+ }
+ },
+
+ "expected": {
+ "photos" : {
+ "1234" : "Acme.jpg",
+ "9876" : "Boxy.jpg"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex1.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex1.json
new file mode 100644
index 00000000..2e812b80
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex1.json
@@ -0,0 +1,23 @@
+{
+ "input": {
+ "data" : {
+ "clientId": "1234",
+ "clientNameStuff" : {
+ "clientName": "Acme",
+ "otherClientStuff" : "pants"
+ }
+ }
+ },
+
+ "spec": {
+ "data" : {
+ "@clientNameStuff.clientName" : "bookMap.@clientId"
+ }
+ },
+
+ "expected": {
+ "bookMap" : {
+ "1234" : "Acme"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex2.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex2.json
new file mode 100644
index 00000000..564126c7
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex2.json
@@ -0,0 +1,24 @@
+{
+ "input": {
+ "data" : {
+ "clientId": "1234",
+ "clientNameStuff" : {
+ "clientName": "Acme",
+ "otherClientStuff" : "pants"
+ }
+ }
+ },
+
+ "spec": {
+ "data" : {
+ // Verify LHS canonical form
+ "@(clientNameStuff.clientName)" : "bookMap.@clientId"
+ }
+ },
+
+ "expected": {
+ "bookMap" : {
+ "1234" : "Acme"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex3_both-sides-multipart.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex3_both-sides-multipart.json
new file mode 100644
index 00000000..ccffc815
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex3_both-sides-multipart.json
@@ -0,0 +1,29 @@
+{
+ "input": {
+ "data" : {
+ "clientIdStuff" : {
+ "clientId": "1234",
+ "orderIdStuff" : "shoes"
+ },
+ "clientNameStuff" : {
+ "clientName": "Acme",
+ "otherClientStuff" : "pants"
+ }
+ }
+ },
+
+ "spec": {
+ "data" : {
+ // Verify we can write data out to a "literal.@().literal" location
+ "@(clientNameStuff.clientName)" : "data.@(clientIdStuff.clientId).clientName"
+ }
+ },
+
+ "expected": {
+ "data" : {
+ "1234": {
+ "clientName": "Acme"
+ }
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex4_lhs-multipart-rhs-sugar.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex4_lhs-multipart-rhs-sugar.json
new file mode 100644
index 00000000..3f53db3e
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex4_lhs-multipart-rhs-sugar.json
@@ -0,0 +1,26 @@
+{
+ "input": {
+ "data" : {
+ "clientId": "1234",
+ "clientNameStuff" : {
+ "clientName": "Acme",
+ "otherClientStuff" : "pants"
+ }
+ }
+ },
+
+ "spec": {
+ "data" : {
+ // Verify we can write data out to a RHS with syntactic sugar @ in the middle of two literals: "literal.@SUGAR.literal"
+ "@(clientNameStuff.clientName)" : "data.@clientId.clientName"
+ }
+ },
+
+ "expected": {
+ "data" : {
+ "1234": {
+ "clientName": "Acme"
+ }
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex5_at-logic-with-embedded-array-lookups.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex5_at-logic-with-embedded-array-lookups.json
new file mode 100644
index 00000000..50e2980b
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex5_at-logic-with-embedded-array-lookups.json
@@ -0,0 +1,23 @@
+{
+ "input": {
+ "sillyPhotoData" : {
+ "captions" : [ "Hi!", "Sunny" ],
+ "fileNames" : [ "hola.jpg", "sunny.jpg"]
+ }
+ },
+
+ "spec": {
+ "sillyPhotoData" : {
+ // This is a rather silly test, but it does prove you can have [0] inside an @()
+ "@(fileNames[0])" : "sillyPhotoData.@(captions[0])",
+ "@(fileNames[1])" : "sillyPhotoData.@(captions[1])"
+ }
+ },
+
+ "expected": {
+ "sillyPhotoData" : {
+ "Hi!" : "hola.jpg",
+ "Sunny" : "sunny.jpg"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex6_rhs-complex-at.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex6_rhs-complex-at.json
new file mode 100644
index 00000000..8f1a9b8c
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex6_rhs-complex-at.json
@@ -0,0 +1,34 @@
+{
+ "input": {
+ "data" : {
+ "clientIdStuff" : {
+ "clientId": "1234"
+ },
+ "clientNameStuff" : {
+ "clientName": "Acme",
+ "otherClientStuff" : "pants"
+ }
+ }
+ },
+
+ "spec": {
+ "data" : {
+ "clientNameStuff" : {
+ // walk down the tree till we find a clientName
+ // 1) go two levels up the tree the "2" from "@(2,...)"
+ // 2) then walk down to find a clientId : "1234"
+ // 3) use the "1234" value as part of the output path
+ "clientName" : "data.@(2,clientIdStuff.clientId).clientName"
+ }
+ }
+ },
+
+ "expected": {
+ "data" : {
+ "1234": { // in this case we want the clientId (1234) to be a level in the output tree
+ // rather than just a key/value pair.
+ "clientName": "Acme"
+ }
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex7_coerce-int-string-conversion.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex7_coerce-int-string-conversion.json
new file mode 100644
index 00000000..1293dd6a
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex7_coerce-int-string-conversion.json
@@ -0,0 +1,34 @@
+{
+ "input": {
+ "data" : {
+ "clientIdStuff" : {
+ "clientId": 1234
+ },
+ "clientNameStuff" : {
+ "clientName": "Acme",
+ "otherClientStuff" : "pants"
+ }
+ }
+ },
+
+ "spec": {
+ "data" : {
+ "clientNameStuff" : {
+ // walk down the tree till we find a clientName
+ // 1) go two levels up the tree the "2" from "@(2,...)"
+ // 2) then walk down to find a clientId : 1234
+ // 3) coerce the number 1234, into a String "1234", and use that as part of the output path
+ "clientName" : "data.@(2,clientIdStuff.clientId).clientName"
+ }
+ }
+ },
+
+ "expected": {
+ "data" : {
+ "1234": { // in this case we want the clientId (1234) to be a level in the output tree
+ // rather than just a key/value pair.
+ "clientName": "Acme"
+ }
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex8_coerce-boolean-string-conversion.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex8_coerce-boolean-string-conversion.json
new file mode 100644
index 00000000..2a58dfef
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex8_coerce-boolean-string-conversion.json
@@ -0,0 +1,33 @@
+{
+ "input": {
+ "clients" : {
+ "Acme" : {
+ "clientId": "guid",
+ "enabled" : true
+ },
+ "Axe" : {
+ "clientId": 2,
+ "enabled" : false
+ },
+ "Bob's Burgers" : {
+ "clientId": 3,
+ "enabled" : true
+ }
+ }
+ },
+
+ "spec": {
+ "clients" : {
+ "*": { // clientName
+ "clientId": "clientsById.@(1,enabled)[]" // coerce the boolean into the string "true" or "false"
+ }
+ }
+ },
+
+ "expected": {
+ "clientsById" : {
+ "true" : [ "guid", 3 ], // Note the Ids, whatever they are get passed along, because they are data
+ "false" : [ 2 ]
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeInverseMap1.json b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap1.json
new file mode 100644
index 00000000..1c55d438
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap1.json
@@ -0,0 +1,38 @@
+{
+ "input": {
+ "data" : [
+ {
+ "clientId": "1",
+ "clientName": "Acme",
+ "otherStuff" : "Boom"
+ },
+ {
+ "clientId": "2",
+ "clientName": "Bob's",
+ "otherStuff" : "Burgers"
+ }
+ ]
+ },
+
+ "spec": {
+ "data" : {
+ // We can use the RHS @ sign to look down the tree to find a key for the output
+ "*" : "data.@clientId"
+ }
+ },
+
+ "expected": {
+ "data" : {
+ "1" : {
+ "clientId": "1",
+ "clientName": "Acme",
+ "otherStuff" : "Boom"
+ },
+ "2" : {
+ "clientId": "2",
+ "clientName": "Bob's",
+ "otherStuff" : "Burgers"
+ }
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeInverseMap2.json b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap2.json
new file mode 100644
index 00000000..15cc196a
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap2.json
@@ -0,0 +1,25 @@
+{
+ "input": {
+ "data" : [
+ {
+ "clientId": "1",
+ "clientName": "Acme",
+ "otherStuff" : "Boom"
+ },
+ {
+ "clientId": "2",
+ "clientName": "Bob's",
+ "otherStuff" : "Burgers"
+ }
+ ]
+ },
+
+ "spec": {
+ "data" : {
+ // Verify that if the RHS @ does not find anything, nothing get written
+ "*" : "data.@pants"
+ }
+ },
+
+ "expected": null
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeLHS1.json b/jolt-core/src/test/resources/json/shiftr/transposeLHS1.json
new file mode 100644
index 00000000..d7c15b84
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeLHS1.json
@@ -0,0 +1,18 @@
+{
+ "input": {
+ "data" : {
+ "clientId": "1234",
+ "clientName": "Acme"
+ }
+ },
+
+ "spec": {
+ "data" : {
+ "@clientName" : "CLIENTNAME" // verify that we can look down the tree
+ }
+ },
+
+ "expected": {
+ "CLIENTNAME" : "Acme"
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeLHS2.json b/jolt-core/src/test/resources/json/shiftr/transposeLHS2.json
new file mode 100644
index 00000000..e24b470c
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeLHS2.json
@@ -0,0 +1,18 @@
+{
+ "input": {
+ "data" : {
+ "clientId": "1234",
+ "data": "Acme"
+ }
+ },
+
+ "spec": {
+ "data" : {
+ "@&" : "CLIENTNAME" // verify that we can look down the tree, using refs
+ }
+ },
+
+ "expected": {
+ "CLIENTNAME" : "Acme"
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeLHS3.json b/jolt-core/src/test/resources/json/shiftr/transposeLHS3.json
new file mode 100644
index 00000000..867bb238
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeLHS3.json
@@ -0,0 +1,22 @@
+{
+ "input": {
+ "data" : [
+ {
+ "clientId": "1234",
+ "data": "Acme"
+ }
+ ]
+ },
+
+ "spec": {
+ "*" : { // catch the data
+ "*": { // for all array elements
+ "@&1": "CLIENTNAME" // match the key "data" based on what the key was two levels up
+ }
+ }
+ },
+
+ "expected": {
+ "CLIENTNAME" : "Acme"
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeSimple1.json b/jolt-core/src/test/resources/json/shiftr/transposeSimple1.json
new file mode 100644
index 00000000..e48120d4
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeSimple1.json
@@ -0,0 +1,18 @@
+{
+ "input": {
+ "data" : {
+ "clientId": "1234",
+ "clientName": "Acme"
+ }
+ },
+
+ "spec": {
+ "data" : {
+ "@clientName" : "@clientId"
+ }
+ },
+
+ "expected": {
+ "1234" : "Acme"
+ }
+}
\ No newline at end of file
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeSimple2.json b/jolt-core/src/test/resources/json/shiftr/transposeSimple2.json
new file mode 100644
index 00000000..dc0d6988
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeSimple2.json
@@ -0,0 +1,20 @@
+{
+ "input": {
+ "data": {
+ "clientId": "1234",
+ "clientName": "Acme"
+ }
+ },
+
+ "spec": {
+ "data": {
+ "@clientName": "bookMap.@clientId"
+ }
+ },
+
+ "expected": {
+ "bookMap": {
+ "1234": "Acme"
+ }
+ }
+}
diff --git a/jolt-core/src/test/resources/json/shiftr/transposeSimple3.json b/jolt-core/src/test/resources/json/shiftr/transposeSimple3.json
new file mode 100644
index 00000000..5f1153d9
--- /dev/null
+++ b/jolt-core/src/test/resources/json/shiftr/transposeSimple3.json
@@ -0,0 +1,14 @@
+{
+ "input": {
+ "clientId": "1234",
+ "clientName": "Acme"
+ },
+
+ "spec": {
+ "@clientName" : "@clientId" // verify that data can be transposed at the root level
+ },
+
+ "expected": {
+ "1234" : "Acme"
+ }
+}
\ No newline at end of file