All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bazaarvoice.jolt.Defaultr Maven / Gradle / Ivy

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

import com.bazaarvoice.jolt.defaultr.Key;
import com.bazaarvoice.jolt.exception.SpecException;
import com.bazaarvoice.jolt.exception.TransformException;

import javax.inject.Inject;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Defaultr is a kind of JOLT transform that applies default values in a non-destructive way.
 *
 * For comparison :
 * Shitr walks the input data and asks its spec "Where should this go?"
 * Defaultr walks the spec and asks "Does this exist in the data?  If not, add it."
 *
 * Example : Given input JSON like
 * 
 * {
 *   "Rating":3,
 *   "SecondaryRatings":{
 *      "quality":{
 *         "Range":7,
 *         "Value":3,
 *         "Id":"quality"
 *      },
 *      "sharpness": {
 *         "Value":4,
 *         "Id":"sharpness"
 *      }
 *   }
 * }
 * 
* With the desired output being : *
 * {
 *   "Rating":3,
 *   "RatingRange" : 5,
 *   "SecondaryRatings":{
 *      "quality":{
 *         "Range":7,
 *         "Value":3,
 *         "Id":"quality",
 *         "ValueLabel": null,
 *         "Label": null,
 *         "MaxLabel": "Great",
 *         "MinLabel": "Terrible",
 *         "DisplayType": "NORMAL"
 *      },
 *      "sharpness": {
 *         "Range":5,
 *         "Value":4,
 *         "Id":"sharpness",
 *         "ValueLabel": null,
 *         "Label": null,
 *         "MaxLabel": "High",
 *         "MinLabel": "Low",
 *         "DisplayType": "NORMAL"
 *      }
 *   }
 * }
 * 
* This is what the Defaultr Spec would look like *
 * {
 *   "RatingRange" : 5,
 *   "SecondaryRatings": {
 *     "quality|value" : {
 *        "ValueLabel": null,
 *        "Label": null,
 *        "MaxLabel": "Great",
 *        "MinLabel": "Terrible",
 *        "DisplayType": "NORMAL"
 *
 *     }
 *     "*": {
 *        "Range" : 5,
 *        "ValueLabel": null,
 *        "Label": null,
 *        "MaxLabel": "High",
 *        "MinLabel": "Low",
 *        "DisplayType": "NORMAL"
 *     }
 *   }
 * }
 * 
* * The Spec file format for Defaulr a tree Map objects. Defaultr handles outputting * of JSON Arrays via special wildcard in the Spec. * * Defaltr Spec WildCards and Flag : * "*" aka STAR : Apply these defaults to all input keys at this level * "|" aka OR : Apply these defaults to input keys, if they exist * "[]" aka : Signal to Defaultr that the data for this key should be an array. * This means all defaultr keys below this entry have to be "integers". * * Valid Array Specification : *
 * {
 *   "photos[]" : {
 *     "2" : {
 *       "url" : "http://www.bazaarvoice.com",
 *       "caption" : ""
 *     }
 *   }
 * }
 * 
* * An Invalid Array Specification would be : *
 * {
 *   "photos[]" : {
 *     "photo-id-1234" : {
 *       "url" : "http://www.bazaarvoice.com",
 *       "caption" : ""
 *     }
 *   }
 * }
 * 
* * Algorithm * Defaultr walks its Spec in a depth first way. * At each level in the Spec tree, Defaultr, works from most specific to least specific Spec key: * Literals key values * "|", sub-sorted by how many or values there, then alphabetically (for deterministic behavior) * "*" * * At a given level in the Defaultr Spec tree, only literal keys force Defaultr to create new entries * in the input data: either as a single literal value or adding new nested Array or Map objects. * The wildcard operators, are applied after the literal keys, and will not cause the those keys to be * added if they are not already present in the input document (either naturally or having been defaulted * in from literal spec keys). * * * Algorithm : * 1) Walk the spec * 2) for each literal key in the spec (specKey) * 2.1) if the the specKey is a map or array, and the input is null, default an empty Map or Array into the output * 2.2.1) re-curse on the literal spec * 2.2) if the the specKey is a map or array, and the input is not null, but of the "wrong" type, skip and do not recurse * 2.2) if the the specKey, is a literal value, default the literal and value into the output and do not recurse * 3) for each wildcard in the spec * 3.1) find all keys from the defaultee that match the wildcard * 3.2) treat each key as a literal speckey * * Corner Cases : * * Due to Defaultr's array syntax, we can't actually express that we expect the top level of the input to be an Array. * The workaround for this is that we check the type of the object that is at the root level of the input. * If it is a map, no problem. * If it is an array, we treat the "root" level of the Defaultr spec, as if it were the child of an Array type Defaultr entry. * To force unambiguity, Defaultr throws an Exception if the input is null. */ public class Defaultr implements SpecDriven, Transform { public interface WildCards { public static final String STAR = "*"; public static final String OR = "|"; public static final String ARRAY = "[]"; } private final Key mapRoot; private final Key arrayRoot; /** * Configure an instance of Defaultr with a spec. * * @throws SpecException for a malformed spec or if there are issues */ @Inject public Defaultr( Object spec ) { String rootKey = "root"; // Due to defaultr's array syntax, we can't actually express that we expect the top level of the defaultee to be an array, until we see the input. // Thus, in order to have parsed the spec so that we can perform many transforms, we create to specs, one where the root of the input // is a map, and the other where the root of the input is an array. // TODO : Handle arrays better, maybe by having a parent reference in the keys, or ditch the feature of having input that is at top level an array { Map rootSpec = new LinkedHashMap<>(); rootSpec.put( rootKey, spec ); mapRoot = Key.parseSpec( rootSpec ).iterator().next(); } // Thus we check the top level type of the input. { Map rootSpec = new LinkedHashMap<>(); rootSpec.put( rootKey + WildCards.ARRAY, spec ); Key tempKey = null; try { tempKey = Key.parseSpec( rootSpec ).iterator().next(); } catch ( NumberFormatException nfe ) { // this is fine, it means the top level spec has non numeric keys // if someone passes a top level array as input later we will error then } arrayRoot = tempKey; } } /** * Top level standalone Defaultr method. * * @param input JSON object to have defaults applied to. This will be modified. * @return the modified input */ @Override public Object transform( Object input ) { if ( input == null ) { // if null, assume HashMap input = new HashMap(); } // TODO : Make copy of the defaultee or like shiftr create a new output object if ( input instanceof List ) { if ( arrayRoot == null ) { throw new TransformException( "The Spec provided can not handle input that is a top level Json Array." ); } arrayRoot.applyChildren( input ); } else { mapRoot.applyChildren( input ); } return input; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy