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

com.arakelian.jq.JqRequest Maven / Gradle / Ivy

The newest version!
/*
 * 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.jq;

import static java.util.logging.Level.FINE;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;

import org.immutables.value.Value;

import com.arakelian.jq.JqLibrary.Jv;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;

@Value.Immutable
public abstract class JqRequest {
    public enum Indent {
        NONE, //
        TAB, //
        SPACE, //
        TWO_SPACES;
    }

    private static final Logger LOGGER = Logger.getLogger(JqRequest.class.getName());

    /**
     * JQ is not thread-safe - https://github.com/stedolan/jq/issues/120
     */
    private static final ReentrantLock SYNC = new ReentrantLock();

    public final JqResponse execute() {
        SYNC.lock();
        try {
            return jq();
        } finally {
            SYNC.unlock();
        }
    }

    @Value.Default
    public Map getArgJson() {
        return ImmutableMap.of();
    }

    @Value.Derived
    @Value.Auxiliary
    public int getDumpFlags() {
        int flags = 0;

        if (isPretty()) {
            flags |= JqLibrary.JV_PRINT_PRETTY;
        }
        switch (getIndent()) {
        case TAB:
            flags = JqLibrary.JV_PRINT_TAB;
            break;
        case SPACE:
            flags = JqLibrary.JV_PRINT_SPACE1;
            break;
        case TWO_SPACES:
            flags = JqLibrary.JV_PRINT_SPACE2;
            break;
        case NONE:
        default:
            break;
        }

        if (isSortKeys()) {
            flags |= JqLibrary.JV_PRINT_SORTED;
        }
        return flags;
    }

    @Value.Default
    public String getFilter() {
        return ".";
    }

    @Value.Default
    public Indent getIndent() {
        return Indent.TWO_SPACES;
    }

    public abstract String getInput();

    public abstract JqLibrary getLib();

    public abstract List getModulePaths();

    @Value.Default
    public String getStreamSeparator() {
        return "\n";
    }

    @Value.Default
    public boolean isPretty() {
        return true;
    }

    @Value.Default
    public boolean isSortKeys() {
        return false;
    }

    /**
     * Adds any messages produced by jq native code it to the error store, with the provided prefix.
     *
     * @param value
     *            value reference
     */
    private String getInvalidMessage(final Jv value) {
        final Jv copy = getLib().jv_copy(value);
        if (getLib().jv_invalid_has_msg(copy)) {
            final Jv message = getLib().jv_invalid_get_msg(value);
            return getLib().jv_string_value(message);
        } else {
            getLib().jv_free(value);
            return null;
        }
    }

    private boolean isValid(final ImmutableJqResponse.Builder response, final Jv value) {
        if (getLib().jv_is_valid(value)) {
            return true;
        }

        // success finishes will return "invalid" value without a message
        final String message = getInvalidMessage(value);
        if (message != null) {
            response.addError(message);
        }
        return false;
    }

    private JqResponse jq() {
        LOGGER.log(FINE, "Initializing JQ");
        final JqLibrary lib = getLib();
        final Pointer jq = lib.jq_init();
        Preconditions.checkState(jq != null, "jq must be non-null");

        Jv moduleDirs = lib.jv_array();
        for (final File file : getModulePaths()) {
            try {
                final String dir = file.getCanonicalPath();
                LOGGER.log(FINE, "Using module path: " + dir);
                moduleDirs = lib.jv_array_append(moduleDirs, lib.jv_string(dir));
            } catch (final IOException e) {
                throw new UncheckedIOException(e);
            }
        }
        lib.jq_set_attr(jq, lib.jv_string("JQ_LIBRARY_PATH"), moduleDirs);

        try {
            final JqResponse response = parse(jq);
            LOGGER.log(FINE, "Response ready");
            return response;
        } finally {
            LOGGER.log(FINE, "Releasing JQ");
            lib.jq_teardown(jq);
            LOGGER.log(FINE, "JQ released successfully");
        }
    }

    private JqResponse parse(final Pointer jq) {
        final ImmutableJqResponse.Builder response = ImmutableJqResponse.builder();

        LOGGER.log(FINE, "Configuring callback");
        final JqLibrary lib = getLib();
        lib.jq_set_error_cb(jq, (data, jv) -> {
            LOGGER.log(FINE, "Error callback");
            final int kind = lib.jv_get_kind(jv);
            if (kind == JqLibrary.JV_KIND_STRING) {
                final String error = lib.jv_string_value(jv).replaceAll("\\s++$", "");
                response.addError(error);
            }
        }, new Pointer(0));

        // for JQ 1.5, arguments is an array; this changes with JQ 1.6+
        Jv args = lib.jv_object();

        final Map argJson = getArgJson();
        for (final String varname : argJson.keySet()) {
            final String text = argJson.get(varname);

            final Jv json = lib.jv_parse(text);
            if (!lib.jv_is_valid(json)) {
                response.addError("Invalid JSON text passed to --argjson (name: " + varname + ")");
                return response.build();
            }

            args = lib.jv_object_set(args, lib.jv_string(varname), json);
        }

        try {
            // compile JQ program
            LOGGER.log(FINE, "Compiling filter");
            final String filter = getFilter();
            if (!lib.jq_compile_args(jq, filter, lib.jv_copy(args))) {
                // compile errors are captured by callback
                LOGGER.log(FINE, "Compilation failed");
                return response.build();
            }

            // create JQ parser
            LOGGER.log(FINE, "Creating parse");
            final int parserFlags = 0;
            final Pointer parser = lib.jv_parser_new(parserFlags);
            try {
                parse(jq, parser, getInput(), response);
                return response.build();
            } finally {
                LOGGER.log(FINE, "Releasing parser");
                lib.jv_parser_free(parser);
            }
        } finally {
            LOGGER.log(FINE, "Releasing callback");
            lib.jq_set_error_cb(jq, null, null);
        }
    }

    /**
     * Add the contents of a native memory array as text to the next chunk of input of the jq
     * program.
     *
     * @param jq
     *            JQ instance
     * @param parser
     *            JQ parser
     * @param text
     *            input JSON
     * @param response
     *            response that we are building
     */
    private void parse(
            final Pointer jq,
            final Pointer parser,
            final String text,
            final ImmutableJqResponse.Builder response) {
        final byte[] input = text.getBytes(Charsets.UTF_8);
        final Memory memory = new Memory(input.length);
        memory.write(0, input, 0, input.length);

        // give text to JQ parser
        LOGGER.log(FINE, "Sending text to parser");
        getLib().jv_parser_set_buf(parser, memory, input.length, true);

        final int flags = getDumpFlags();
        final StringBuilder buf = new StringBuilder();
        for (;;) {
            // iterate until JQ consumes all inputs
            LOGGER.log(FINE, "Parsing text");
            final Jv parsed = getLib().jv_parser_next(parser);
            if (!isValid(response, parsed)) {
                break;
            }

            // iterate until we consume all JQ streams
            // see: https://stedolan.github.io/jq/tutorial/
            LOGGER.log(FINE, "Consuming JQ response");
            getLib().jq_start(jq, parsed);
            for (;;) {
                final Jv next = getLib().jq_next(jq);
                if (!isValid(response, next)) {
                    break;
                }

                LOGGER.log(FINE, "Dumping response");
                final String out = getLib().jv_dump_string(next, flags);
                if (buf.length() != 0) {
                    buf.append(getStreamSeparator());
                }
                buf.append(out);
            }
        }

        // tell parser we are finished
        LOGGER.log(FINE, "Finishing with parser");

        // finalize output
        final String output = buf.toString();
        response.output(output);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy