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

com.arakelian.elastic.model.JsonSelector Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.arakelian.elastic.model;

import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.immutables.value.Value;

import com.arakelian.elastic.utils.JsonNodeUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.JsonPath;

@Value.Immutable
@JsonSerialize(as = ImmutableJsonSelector.class)
@JsonDeserialize(builder = ImmutableJsonSelector.Builder.class)
public abstract class JsonSelector implements Serializable {
    public static enum Type {
        PATH, JSON_PATH, FUNCTION, CONCAT;
    }

    /** Regular expression to match an identifier **/
    private static final String IDENTIFIER = "(?:\\b[_a-zA-Z]|\\B)[_a-zA-Z0-9]*+";

    /** Match @FUNCTION name at start of selector **/
    static final Pattern FUNCTION = Pattern.compile("^\\@(" + IDENTIFIER + ")\\b");

    /**
     * Break part a path that uses forward slash or dot notation.
     *
     * Note: By omitting empty strings, we collapse multiple dots or slashes with nothing in
     * between. We do this to be forgiving in the input.
     **/
    private static final Splitter PATH_SPLITTER = Splitter //
            .on(Pattern.compile("[/\\.]")) //
            .trimResults() //
            .omitEmptyStrings();

    /** Break apart a comma separated argument list **/
    private static final Splitter ARG_SPLITTER = Splitter //
            .on(Pattern.compile("\\s*,\\s*")) //
            .trimResults();

    /**
     * Used to build a canonical path, with clean separators
     */
    private static final Joiner PATH_JOINER = Joiner.on("/");

    /**
     * Used to build a canonical path, with clean separators
     */
    private static final Joiner ARG_JOINER = Joiner.on(", ");

    public static JsonSelector of(final String selector) {
        return ImmutableJsonSelector.builder() //
                .selector(selector) //
                .build();
    }

    private static List toPath(final String selector) {
        // split path on / or .
        final List path = PATH_SPLITTER.splitToList(selector);

        // path cannot be empty
        final int length = path.size();
        Preconditions.checkState(length != 0, "path cannot be empty");
        return ImmutableList.copyOf(path);
    }

    @JsonIgnore
    @Value.Lazy
    @Value.Auxiliary
    public Map> getArguments() {
        final Type type = getType();

        final int start;
        if (type == Type.FUNCTION) {
            // skip @ sign
            start = getFunctionName().length() + 1;
        } else if (type == Type.CONCAT) {
            // skip + sign
            start = 1;
        } else {
            return ImmutableMap.of();
        }

        final ImmutableMap.Builder> map = ImmutableMap.builder();
        for (final String arg : ARG_SPLITTER.splitToList(StringUtils.substring(getSelector(), start))) {
            final List path = toPath(arg);
            final String normalizePath = PATH_JOINER.join(path);
            map.put(normalizePath, path);
        }
        return map.build();
    }

    @JsonIgnore
    @Value.Lazy
    @Value.Auxiliary
    public String getFunctionName() {
        Preconditions.checkState(getType() == Type.FUNCTION);
        final String selector = getSelector();
        final Matcher matcher = FUNCTION.matcher(selector);
        Preconditions.checkState(matcher.find(), "Invalid function name: " + selector);
        return matcher.group(1);
    }

    @JsonIgnore
    @Value.Lazy
    @Value.Auxiliary
    public JsonPath getJsonPath() throws IllegalStateException {
        Preconditions.checkState(getType() == Type.JSON_PATH);
        try {
            return JsonPath.compile(getSelector());
        } catch (final InvalidPathException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    @JsonIgnore
    @Value.Lazy
    @Value.Auxiliary
    public List getPath() {
        Preconditions.checkState(getType() == Type.PATH);
        final String selector = getSelector();
        return toPath(selector);
    }

    public abstract String getSelector();

    @JsonIgnore
    @Value.Lazy
    @Value.Auxiliary
    public Type getType() throws IllegalStateException {
        final String selector = getSelector();

        // must have a path
        if (StringUtils.isEmpty(selector)) {
            throw new IllegalStateException("selector must be non-empty");
        }

        // quick test to see if explicit JsonPath
        final char first = selector.charAt(0);
        switch (first) {
        case '$':
            return Type.JSON_PATH;
        case '@':
            return Type.FUNCTION;
        case '+':
            return Type.CONCAT;
        default:
            return Type.PATH;
        }
    }

    @Value.Check
    public JsonSelector normalizeSelector() {
        final String val = getSelector();
        final String newVal;

        try {
            final Type type = getType();

            switch (type) {
            case PATH:
                newVal = PATH_JOINER.join(getPath());
                break;
            case JSON_PATH:
                newVal = getJsonPath().getPath();
                break;
            case CONCAT:
                newVal = "+ " + ARG_JOINER.join(getArguments().keySet());
                break;
            case FUNCTION:
                newVal = "@" + getFunctionName() + " " + ARG_JOINER.join(getArguments().keySet());
                break;
            default:
                newVal = val;
            }
        } catch (final IllegalStateException e) {
            // cannot normalize invalid selector
            return this;
        }

        if (!StringUtils.equals(val, newVal)) {
            return ((ImmutableJsonSelector) this).withSelector(newVal);
        }
        return this;
    }

    public JsonNode read(final JsonNode node) {
        Preconditions.checkState(getType() == Type.PATH);
        return JsonNodeUtils.read(node, getPath());
    }

    public void read(final JsonNode node, final Consumer consumer) {
        Preconditions.checkState(getType() == Type.PATH);
        JsonNodeUtils.read(node, consumer, getPath());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy