bazaarvoice / jolt

JSON to JSON transformation library written in Java.
Apache License 2.0
1.56k stars 329 forks source link

jolt custom transformation support in springboot application #1185

Open Prasaddiwalkar opened 1 year ago

Prasaddiwalkar commented 1 year ago

I have following xml file from where I need to transform few properties. when I try to inject bean in CountryListBean in CountryCodeTransformer it does not instantiate as I suspect JOLT custom transformation missing the SpringBoot context while performing transformation.

any clue how to carry-forward SpringBoot context so that jolt custom transformer can get injected bean properly

<?xml version="1.0" encoding="utf-8"?>
<CountryList xmlns="http://www.org.com/tc/CountryList">
  <countries>
    <countryName>Afghanistan</countryName>
    <eCountryName>Afghanistan</eCountryName>
    <twoCharCode>AF</twoCharCode>
    <threeCharCode>AFG</threeCharCode>
  </countries>
  <countries>
    <countryName>Albania</countryName>
    <eCountryName>Albania</eCountryName>
    <twoCharCode>AL</twoCharCode>
    <threeCharCode>ALB</threeCharCode>
  </countries>
</CountryList>

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified"
  elementFormDefault="qualified"
  targetNamespace="http://www.org.com/tc/CountryList"
  xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="CountryList">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" name="countries">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="countryName" type="xs:string" />
              <xs:element name="eCountryName"
                type="xs:string" />
              <xs:element name="twoCharCode" type="xs:string" />
              <xs:element name="threeCharCode" type="xs:string" />
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

import java.util.Map;

import org.springframework.stereotype.Component;

import com.org.xsd.CountryList.Countries;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@EqualsAndHashCode
public class CountryListBean {

    private Map<String, Countries> countryToCode;
}
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.org.CountryListBean;

import com.org.xsd.CountryList.Countries;
import com.fasterxml.jackson.core.exc.StreamReadException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

@Getter
@Setter
@Configuration
@Slf4j
public class LookUpListConfigurtion {

    @Bean
    public CountryListBean countryConfiguration() {

        InputStream is = LookUpListConfigurtion.class.getResourceAsStream("/mapping/CountryList.xml");

        XmlMapper xmlMapper = new XmlMapper();

        List<Countries> countries = null;
        try {
            countries = xmlMapper.readValue(is, new TypeReference<List<Countries>>() {
            });
        } catch (StreamReadException e) {
            e.printStackTrace();
        } catch (DatabindException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        Map<String, Countries> nameToCountry = countries.stream()
                .collect(Collectors.toMap(Countries::getECountryName, Function.identity()));

        CountryListBean bean = new CountryListBean();
        bean.setCountryToCode(nameToCountry);

        return bean;
    }

}
public class CountryCodeTransformer {

    @Component
    public static final class countryCode extends ListFunction {
    @Autowired
    CountryListBean bean;
        @Override
        protected Optional<Object> applyList(List<Object> arg0) {

            return Optional.of(String.valueOf(bean.getCountryToCode().get(arg0.get(0)).getTwoCharCode()));
        }

    }

}

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import com.bazaarvoice.jolt.ContextualTransform;
import com.bazaarvoice.jolt.SpecDriven;
import com.bazaarvoice.jolt.common.Optional;
import com.bazaarvoice.jolt.common.tree.MatchedElement;
import com.bazaarvoice.jolt.common.tree.WalkedPath;
import com.bazaarvoice.jolt.exception.SpecException;
import com.bazaarvoice.jolt.modifier.OpMode;
import com.bazaarvoice.jolt.modifier.TemplatrSpecBuilder;
import com.bazaarvoice.jolt.modifier.function.Function;
import com.bazaarvoice.jolt.modifier.spec.ModifierCompositeSpec;

public class CountryTransformerSpec implements SpecDriven, ContextualTransform {
    private static final Map<String, Function> STOCK_FUNCTIONS = Map.of(
            "countryCode", new CountryCodeTransformer.countryCode());

    private final ModifierCompositeSpec rootSpec;

    @SuppressWarnings("unchecked")
    private CountryTransformerSpec(Object spec, OpMode opMode, Map<String, Function> functionsMap) {
        if (spec == null) {
            throw new SpecException(opMode.name() + " expected a spec of Map type, got 'null'.");
        }

        if (!(spec instanceof Map)) {
            throw new SpecException(
                    opMode.name() + " expected a spec of Map type, got " + spec.getClass().getSimpleName());
        }

        if (functionsMap == null || functionsMap.isEmpty()) {
            throw new SpecException(opMode.name() + " expected a populated functions' map type, got "
                    + (functionsMap == null ? "null" : "empty"));
        }

        functionsMap = Collections.unmodifiableMap(functionsMap);
        TemplatrSpecBuilder templatrSpecBuilder = new TemplatrSpecBuilder(opMode, functionsMap);

        rootSpec = new ModifierCompositeSpec(ROOT_KEY, (Map<String, Object>) spec, opMode, templatrSpecBuilder);
    }

    @Override
    public Object transform(final Object input, final Map<String, Object> context) {

        Map<String, Object> contextWrapper = new HashMap<>();
        contextWrapper.put(ROOT_KEY, context);

        MatchedElement rootLpe = new MatchedElement(ROOT_KEY);
        WalkedPath walkedPath = new WalkedPath();
        walkedPath.add(input, rootLpe);

        rootSpec.apply(ROOT_KEY, Optional.of(input), walkedPath, null, contextWrapper);

        return input;
    }

    /**
     * This variant of modifier creates the key/index is missing, and overwrites the
     * value if present
     */
    public static final class Overwritr extends CountryTransformerSpec {

        public Overwritr(Object spec) {
            this(spec, STOCK_FUNCTIONS);
        }

        public Overwritr(Object spec, Map<String, Function> functionsMap) {
            super(spec, OpMode.OVERWRITR, functionsMap);
        }
    }

    /**
     * This variant of modifier only writes when the key/index is missing
     */
    public static final class Definr extends CountryTransformerSpec {

        public Definr(final Object spec) {
            this(spec, STOCK_FUNCTIONS);
        }

        public Definr(Object spec, Map<String, Function> functionsMap) {
            super(spec, OpMode.DEFINER, functionsMap);
        }
    }

    /**
     * This variant of modifier only writes when the key/index is missing or the
     * value is null
     */
    public static class Defaultr extends CountryTransformerSpec {

        public Defaultr(final Object spec) {
            this(spec, STOCK_FUNCTIONS);
        }

        public Defaultr(Object spec, Map<String, Function> functionsMap) {
            super(spec, OpMode.DEFAULTR, functionsMap);
        }
    }
}
{
  countryName: 'Afghanistan'
}
{
  countryName: 'AF'
}
[
  {
     "operation": "com.org.jolt.transformer.CountryTransformerSpec$Overwritr",
     "spec": {
          "countryName": "=countryCode(@(1,countryName) ))"     
      }
    }
]
colonelpopcorn commented 1 year ago

Don't you need an @Inject annotation on your constructors?

I got my custom transformer working by putting the @Inject annotations on the public constructors for Overwritr, Definr, and Defaultr classes. Specifically the public (final Object spec) constructors. To be honest, I'm not sure about the autowiring, though. I'm sure you're hitting the applyList function in a debugger, right? If you are it definitely is an issue with spring boot annotations not being applied, but if you're running in a spring boot application it shouldn't matter.