bazaarvoice / jolt

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

Help to convert timestamp to formatted time string #1091

Open JiexunLi opened 3 years ago

JiexunLi commented 3 years ago

Is there a way to specify something that like

new Instant(timestamp)

to convert timestamp(long) to a date string?

input: { "timestamp": 1623960973468 }

output: { "timestamp": "2021-06-17T20:16:13.468Z" }

lucioalmeida commented 3 years ago

Its only possible with custom modifies.

package br.com.teste.jolt;

import com.bazaarvoice.jolt.common.Optional;
import com.bazaarvoice.jolt.modifier.function.Function;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class CustomFunctions {
    public static final class timestampToDateString extends Function.SingleFunction<String> {

        @Override
        protected Optional<String> applySingle(Object arg) {
            if (arg == null) {
                return Optional.empty();
            }
            Long timestamp = (Long) arg;
            Instant instant = Instant.ofEpochMilli(timestamp);
            DateTimeFormatter isoOffsetDateTime = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault());
            return Optional.of(isoOffsetDateTime.format(instant));
        }
    }
}
package br.com.teste.jolt;

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.Math;
import com.bazaarvoice.jolt.modifier.function.*;
import com.bazaarvoice.jolt.modifier.spec.ModifierCompositeSpec;

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

public class CustomModifier implements SpecDriven, ContextualTransform {

    private static final Map<String, Function> STOCK_FUNCTIONS = new HashMap<>();

    private final ModifierCompositeSpec rootSpec;

    static {
        STOCK_FUNCTIONS.put("timestampToDateString", new CustomFunctions.timestampToDateString());
    }

    @SuppressWarnings("unchecked")
    private CustomModifier(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 CustomModifier {

        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 CustomModifier {

        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 CustomModifier {

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

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

}
[{
    "operation": "br.com.teste.jolt.CustomModifier$Overwritr",
    "spec": {
        "timestamp": "=timestampToDateString(@(1,timestamp))"
    }

}]

EDIT Now is ok.

sudharnath commented 3 years ago

@lucioalmeida I Tried your solution but, Chainr is throwing a class not found exception for the custom Overwritr "br.com.teste.jolt.CustomModifier.Overwritr".

Jolt Chainr could not find transform class br.com.teste.jolt.CustomModifier.Overwritr at index:0.

I checked the package names and the class name, they all match. Please suggest

lucioalmeida commented 3 years ago

CustomFunctions has a bug. This is ok now

public class CustomFunctions {
   public static final class timestampToDateString extends Function.SingleFunction<String> {

       @Override
       protected Optional<String> applySingle(Object arg) {
           if (arg == null) {
               return Optional.empty();
           }
           Long timestamp = (Long) arg;
           Instant instant = Instant.ofEpochMilli(timestamp);
           DateTimeFormatter isoOffsetDateTime = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault());
           return Optional.of(isoOffsetDateTime.format(instant));
       }
   }
}

The class name is ok now

[{
    "operation": "br.com.teste.jolt.CustomModifier$Overwritr",
    "spec": {
        "timestamp": "=timestampToDateString(@(1,timestamp))"
    }

}]
sudharnath commented 3 years ago

That worked great 👍 .

Can I pass any arguments to the SingleFunction such as the date format where my spec can include the target date format. `[{ "operation": "br.com.teste.jolt.CustomModifier$Overwritr", "spec": { "timestamp": "=timestampToDateString(@(1,timestamp, "MM-dd-yyyy hh:mm:ss"))" }

}] `

lucioalmeida commented 3 years ago

Is possible with this

package br.com.teste.jolt;

import com.bazaarvoice.jolt.common.Optional;
import com.bazaarvoice.jolt.modifier.function.Function;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;

public class CustomFunctions {
   public static final class timestampToDateString extends Function.ListFunction {

       @Override
       protected Optional<Object> applyList(List<Object> argList) {
           if (argList.size() < 2) {
               return Optional.empty();
           }
           Long timestamp = (Long) argList.get(0);
           String pattern  = (String) argList.get(1);
           Instant instant = Instant.ofEpochMilli(timestamp);
           DateTimeFormatter isoOffsetDateTime = DateTimeFormatter.ofPattern(pattern).withZone(ZoneId.systemDefault());
           return Optional.of(isoOffsetDateTime.format(instant));
       }
   }
}

Spec:

[{
    "operation": "br.com.teste.jolt.CustomModifier$Overwritr",
    "spec": {
        "timestamp": "=timestampToDateString(@(1,timestamp), 'MM-dd-yyyy hh:mm:ss')"
    }
}]

Output:

{"timestamp":"06-17-2021 05:16:13"}

EDIT Spec is correct now

sudharnath commented 3 years ago

Thanks for your swift replies. I tried this but the applyList method was never called.

applySingle method works fine though

lucioalmeida commented 3 years ago

Sorry. My spec is wrong

[{
    "operation": "br.com.teste.jolt.CustomModifier$Overwritr",
    "spec": {
        "timestamp": "=timestampToDateString(@(1,timestamp), 'MM-dd-yyyy hh:mm:ss')"
    }
}]
sudharnath commented 3 years ago

@lucioalmeida Awesome, it works now. You saved me a lot of time.

Thank You 👍 :)

lucioalmeida commented 3 years ago

Your welcome. Could you close the issue.

sudharnath commented 3 years ago

I am not able to close it as someone else opened the issue. @JiexunLi Can you please close the issue, if you got what you needed.

Prasaddiwalkar commented 1 year ago

@lucioalmeida I followed exact same steps but custom transformer is not getting hit while transform. Anything missing or modification required?