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

org.apache.felix.hc.generalchecks.HttpRequestsCheck 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 SF 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 org.apache.felix.hc.generalchecks;

import static java.util.stream.StreamSupport.stream;
import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.felix.hc.annotation.HealthCheckService;
import org.apache.felix.hc.api.FormattingResultLog;
import org.apache.felix.hc.api.HealthCheck;
import org.apache.felix.hc.api.Result;
import org.apache.felix.hc.api.ResultLog;
import org.apache.felix.hc.core.impl.util.lang.StringUtils;
import org.apache.felix.hc.generalchecks.util.SimpleConstraintChecker;
import org.apache.felix.utils.json.JSONParser;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@HealthCheckService(name = HttpRequestsCheck.HC_NAME)
@Component(configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true)
@Designate(ocd = HttpRequestsCheck.Config.class, factory = true)
public class HttpRequestsCheck implements HealthCheck {

    private static final Logger LOG = LoggerFactory.getLogger(HttpRequestsCheck.class);

    public static final String HC_NAME = "Http Requests";
    public static final String HC_LABEL = "Health Check: " + HC_NAME;

    @ObjectClassDefinition(name = HC_LABEL, description = "Performs http(s) request(s) and checks the response for return code and optionally checks the response entity")
    public @interface Config {
        @AttributeDefinition(name = "Name", description = "Name of this health check")
        String hc_name() default HC_NAME;

        @AttributeDefinition(name = "Tags", description = "List of tags for this health check, used to select subsets of health checks for execution e.g. by a composite health check.")
        String[] hc_tags() default {};

        @AttributeDefinition(name = "Request Specs", description = "List of requests to be made. Requests specs have two parts: "
                + "Before '=>' can be a simple URL/path with curl-syntax advanced options (e.g. setting a header with -H \"Test: Test val\"), "
                + "after the '=>' it is a simple response code that can be followed ' && MATCHES ' to match the response entity against or other matchers like HEADER, TIME or JSON (see defaults when creating a new configuration for examples).")
        String[] requests() default {
            "/path/example.html",
            "/path/example.html => 200",
            "/protected/example.html => 401",
            "-u admin:admin /protected/example.html => 200",
            "/path/example.html => 200 && MATCHES html title.*",
            "/path/example.html => 200 && MATCHES html title.* && MATCHES anotherRegEx[a-z]",
            "/path/example.html => 200 && HEADER Content-Type MATCHES text/html.*",
            "/path/example.json => 200 && JSON root.arr[3].prop = myval",
            "/path/example-timing-important.html => 200 && TIME < 2000",
            "-X GET -H \"Accept: application/javascript\" http://api.example.com/path/example.json => 200 && JSON root.arr[3].prop = myval",
            "-X HEAD --data \"{....}\" http://www.example.com/path/to/data.json => 303",
            "--proxy proxyhost:2000 /path/example-timing-important.html => 200 && TIME < 2000"
        };

        @AttributeDefinition(name = "Connect Timeout", description = "Default connect timeout in ms. Can be overwritten per request with option --connect-timeout (in sec)")
        int connectTimeoutInMs() default 7000;

        @AttributeDefinition(name = "Read Timeout", description = "Default read timeout in ms. Can be overwritten with per request option -m or --max-time (in sec)")
        int readTimeoutInMs() default 7000;

        @AttributeDefinition(name = "Status for failed request constraint", description = "Status to fail with if the constraint check fails")
        Result.Status statusForFailedContraint() default Result.Status.WARN;

        @AttributeDefinition(name = "Run in parallel", description = "Run requests in parallel (only active if more than one request spec is configured)")
        boolean runInParallel() default true;


        @AttributeDefinition
        String webconsole_configurationFactory_nameHint() default "{hc.name}: {requests}";

    }

    private List requestSpecs;
    private int connectTimeoutInMs;
    private int readTimeoutInMs;
    private Result.Status statusForFailedContraint;
    private boolean runInParallel;

    private String defaultBaseUrl;

    private FormattingResultLog configErrors;

    @Reference
    private ServiceReference httpServiceReference;

    @Activate
    protected void activate(Config config) {
        this.requestSpecs = getRequestSpecs(config.requests());
        this.connectTimeoutInMs = config.connectTimeoutInMs();
        this.readTimeoutInMs = config.readTimeoutInMs();
        this.statusForFailedContraint = config.statusForFailedContraint();
        this.runInParallel = config.runInParallel() && requestSpecs.size() > 1;

        setupDefaultBaseUrl();

        LOG.debug("Default BaseURL: {}", defaultBaseUrl);
        LOG.debug("Activated Requests HC: {}", requestSpecs);
    }

    private void setupDefaultBaseUrl() {
        boolean isHttp = Boolean.parseBoolean(String.valueOf(httpServiceReference.getProperty("org.apache.felix.http.enable")));
        boolean isHttps = Boolean.parseBoolean(String.valueOf(httpServiceReference.getProperty("org.apache.felix.https.enable")));
        if (isHttp) {
            defaultBaseUrl = "http://localhost:"+httpServiceReference.getProperty("org.osgi.service.http.port");
        } else if (isHttps) {
            defaultBaseUrl = "http://localhost:"+httpServiceReference.getProperty("org.osgi.service.https.port");
        }
    }

    @Override
    public Result execute() {

        FormattingResultLog overallLog = new FormattingResultLog();

        // take over config errors
        for(ResultLog.Entry entry: configErrors) {
            overallLog.add(entry);
        }

        // execute requests
        Stream requestSpecsStream = runInParallel ? requestSpecs.parallelStream() : requestSpecs.stream();
        List logsForEachRequest = requestSpecsStream
            .map(requestSpec -> requestSpec.check(defaultBaseUrl, connectTimeoutInMs, readTimeoutInMs, statusForFailedContraint, requestSpecs.size()>1))
            .collect(Collectors.toList());

        // aggregate logs never in parallel
        logsForEachRequest.stream().forEach( l -> stream(l.spliterator(), false).forEach(e -> overallLog.add(e)));

        return new Result(overallLog);

    }

    private List getRequestSpecs(String[] requestSpecStrArr) {

        configErrors = new FormattingResultLog();

        List requestSpecs = new ArrayList();
        for(String requestSpecStr: requestSpecStrArr) {
            try {
                RequestSpec requestSpec = new RequestSpec(requestSpecStr);
                requestSpecs.add(requestSpec);
            } catch(Exception e) {
                configErrors.critical("Invalid config: {}", requestSpecStr);
                configErrors.add(new ResultLog.Entry(Result.Status.CRITICAL, " "+e.getMessage(), e));
            }

        }
        return requestSpecs;
    }

    static class RequestSpec {

        private static final String HEADER_AUTHORIZATION = "Authorization";

        String method = "GET";
        String url;
        Map headers = new HashMap();
        String data = null;

        String user;

        Integer connectTimeoutInMs;
        Integer readTimeoutInMs;

        Proxy proxy;

        List responseChecks = new ArrayList();

        RequestSpec(String requestSpecStr) throws ParseException, URISyntaxException {

            String[] requestSpecBits = requestSpecStr.split(" *=> *", 2);

            String requestInfo = requestSpecBits[0];
            parseCurlLikeRequestInfo(requestInfo);

            if(requestSpecBits.length > 1) {
                parseResponseAssertion(requestSpecBits[1]);
            } else {
                // check for 200 as default
                responseChecks.add(new ResponseCodeCheck(200));
            }
        }

        private void parseResponseAssertion(String responseAssertions) {

            String[] responseAssertionArr = responseAssertions.split(" +&& +");
            for(String clause: responseAssertionArr) {
                if(isNumeric(clause)) {
                    responseChecks.add(new ResponseCodeCheck(Integer.parseInt(clause)));
                } else if(clause.toUpperCase().startsWith(ResponseTimeCheck.TIME)) {
                    responseChecks.add(new ResponseTimeCheck(clause.substring(ResponseTimeCheck.TIME.length())));
                } else if(clause.toUpperCase().startsWith(ResponseEntityRegExCheck.MATCHES)) {
                    responseChecks.add(new ResponseEntityRegExCheck(Pattern.compile(clause.substring(ResponseEntityRegExCheck.MATCHES.length()))));
                } else if(clause.toUpperCase().startsWith(ResponseHeaderCheck.HEADER)) {
                    responseChecks.add(new ResponseHeaderCheck(clause.substring(ResponseHeaderCheck.HEADER.length())));
                } else if(clause.toUpperCase().startsWith(JsonPropertyCheck.JSON)) {
                    responseChecks.add(new JsonPropertyCheck(clause.substring(JsonPropertyCheck.JSON.length())));
                } else {
                    throw new IllegalArgumentException("Invalid response content assertion clause: '"+clause+"'");
                }
            }
        }

        private boolean isNumeric(String text) {
            try {
                Integer.parseInt(text);
                return true;
            } catch(NumberFormatException e) {
                return false;
            }
        }

        private void parseCurlLikeRequestInfo(String requestInfo) throws ParseException, URISyntaxException {
            CommandLineParser parser = new DefaultParser();

            Options options = new Options();
            options.addOption("H", "header", true, "");
            options.addOption("X", "method", true, "");
            options.addOption("d", "data", true, "");
            options.addOption("u", "user", true, "");
            options.addOption(null, "connect-timeout", true, "");
            options.addOption("m", "max-time", true, "");
            options.addOption("x", "proxy", true, "");

            String[] args = splitArgsRespectingQuotes(requestInfo);

            CommandLine line = parser.parse(options, args);

            if (line.hasOption("header")) {
                String[] headerValues = line.getOptionValues("header");
                for(String headerVal: headerValues) {
                    String[] headerBits = headerVal.split(" *: *", 2);
                    headers.put(headerBits[0], headerBits[1]);
                }
            }
            if (line.hasOption("method")) {
                method = line.getOptionValue("method");
            }
            if (line.hasOption("data")) {
                data = line.getOptionValue("data");
            }
            if (line.hasOption("user")) {
                String userAndPw = line.getOptionValue("user");
                user = userAndPw.split(":")[0];
                byte[] encodedUserAndPw = Base64.getEncoder().encode(userAndPw.getBytes());
                headers.put(HEADER_AUTHORIZATION, "Basic "+new String(encodedUserAndPw));
            }

            if (line.hasOption("connect-timeout")) {
                connectTimeoutInMs = Integer.valueOf(line.getOptionValue("connect-timeout")) * 1000;
            }
            if (line.hasOption("max-time")) {
                readTimeoutInMs = Integer.valueOf(line.getOptionValue("max-time")) * 1000;
            }
            if (line.hasOption("proxy")) {
                String curlProxy = line.getOptionValue("proxy");
                if(curlProxy.contains("@")) {
                    throw new IllegalArgumentException("Proxy authentication is not support");
                }
                String proxyHost;
                int proxyPort;
                if(curlProxy.startsWith("http")) {
                    URI uri = new URI(curlProxy);
                    proxyHost = uri.getHost();
                    proxyPort = uri.getPort();
                } else {
                    String[] curlProxyBits = curlProxy.split(":");
                    proxyHost = curlProxyBits[0];
                    proxyPort = curlProxyBits.length > 1 ? Integer.parseInt(curlProxyBits[1]) : 1080;
                }
                proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
            }

            url = line.getArgList().get(0);

        }

        String[] splitArgsRespectingQuotes(String requestInfo) {
            List argList = new ArrayList();
            Pattern regex = Pattern.compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'");
            Matcher regexMatcher = regex.matcher(requestInfo);
            while (regexMatcher.find()) {
                argList.add(regexMatcher.group());
            }
            return argList.toArray(new String[argList.size()]);
        }

        @Override
        public String toString() {
            return "RequestSpec [method=" + method + ", url=" + url + ", headers=" + headers + ", responseChecks=" + responseChecks + "]";
        }

        public FormattingResultLog check(String defaultBaseUrl, int connectTimeoutInMs, int readTimeoutInMs, Result.Status statusForFailedContraint, boolean showTiming) {

            FormattingResultLog log = new FormattingResultLog();
            String urlWithUser = user!=null ? user + " @ " + url: url;
            log.debug("Checking {}", urlWithUser);
            log.debug(" configured headers {}", headers.keySet());

            Response response = null;
            try {
                response = performRequest(defaultBaseUrl, urlWithUser, connectTimeoutInMs, readTimeoutInMs, log);
            } catch (IOException e) {
                // request generally failed
                log.add(new ResultLog.Entry(statusForFailedContraint, urlWithUser+": "+ e.getMessage(), e));
            }

            if(response != null) {
                List resultBits = new ArrayList();
                boolean hasFailed = false;
                for(ResponseCheck responseCheck: responseChecks) {
                    ResponseCheck.ResponseCheckResult result = responseCheck.checkResponse(response, log);
                    hasFailed = hasFailed || result.contraintFailed;
                    resultBits.add(result.message);
                }
                Result.Status status = hasFailed ? statusForFailedContraint : Result.Status.OK;
                String timing = showTiming ? " " + msHumanReadable(response.requestDurationInMs) : "";
                // result of response assertion(s)
                log.add(new ResultLog.Entry(status, urlWithUser+timing+": "+ String.join(", ", resultBits)));
            }

            return log;
        }

        public Response performRequest(String defaultBaseUrl, String urlWithUser, int connectTimeoutInMs, int readTimeoutInMs, FormattingResultLog log) throws IOException {
            Response response = null;
            HttpURLConnection conn = null;
            try {
                URL effectiveUrl;
                if(url.startsWith("/")) {
                    effectiveUrl = new URL(defaultBaseUrl + url);
                } else {
                    effectiveUrl = new URL(url);
                }

                conn = openConnection(connectTimeoutInMs, readTimeoutInMs, effectiveUrl, log);
                response = readResponse(conn, log);

            } finally {
                if(conn!=null) {
                    conn.disconnect();
                }
            }
            return response;
        }

        private HttpURLConnection openConnection(int defaultConnectTimeoutInMs, int defaultReadTimeoutInMs, URL effectiveUrl, FormattingResultLog log)
                throws IOException, ProtocolException {
            HttpURLConnection conn;
            conn = (HttpURLConnection) (proxy==null ? effectiveUrl.openConnection() : effectiveUrl.openConnection(proxy));
            conn.setInstanceFollowRedirects(false);
            conn.setUseCaches(false);

            int effectiveConnectTimeout = this.connectTimeoutInMs !=null ? this.connectTimeoutInMs : defaultConnectTimeoutInMs;
            int effectiveReadTimeout = this.readTimeoutInMs !=null ? this.readTimeoutInMs : defaultReadTimeoutInMs;
            log.debug("connectTimeout={}ms readTimeout={}ms", effectiveConnectTimeout, effectiveReadTimeout);
            conn.setConnectTimeout(effectiveConnectTimeout);
            conn.setReadTimeout(effectiveReadTimeout);

            conn.setRequestMethod(method);
            for(Entry header: headers.entrySet()) {
                conn.setRequestProperty(header.getKey(), header.getValue());
            }
            if(data != null) {
                conn.setDoOutput(true);
                byte[] bytes = data.getBytes();
                log.debug("Sending request entity with {}bytes", bytes.length);
                try(DataOutputStream wr = new DataOutputStream(conn.getOutputStream())) {
                    wr.write(bytes);
                }
            }
            return conn;
        }

        private Response readResponse(HttpURLConnection conn, FormattingResultLog log) throws IOException {

            long startTime = System.currentTimeMillis();

            int actualResponseCode = conn.getResponseCode();
            String actualResponseMessage = conn.getResponseMessage();
            log.debug("Result: {} {}", actualResponseCode, actualResponseMessage);
            Map> responseHeaders = conn.getHeaderFields();

            StringWriter responseEntityWriter = new StringWriter();
            try (BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    responseEntityWriter.write(inputLine + "\n");
                }
            } catch(IOException e) {
                log.debug("Could not get response entity: {}", e.getMessage());
            }

            long requestDurationInMs = System.currentTimeMillis() - startTime;
            Response response = new Response(actualResponseCode, actualResponseMessage, responseHeaders, responseEntityWriter.toString(), requestDurationInMs);

            return response;
        }

    }

    static class Response {
        final int actualResponseCode;
        final String actualResponseMessage;
        final Map> actualResponseHeaders;
        final String actualResponseEntity;
        final long requestDurationInMs;

        public Response(int actualResponseCode, String actualResponseMessage, Map> actualResponseHeaders,
                String actualResponseEntity, long requestDurationInMs) {
            super();
            this.actualResponseCode = actualResponseCode;
            this.actualResponseMessage = actualResponseMessage;
            this.actualResponseHeaders = actualResponseHeaders;
            this.actualResponseEntity = actualResponseEntity;
            this.requestDurationInMs = requestDurationInMs;
        }
    }

    static interface ResponseCheck {

        class ResponseCheckResult {
            final boolean contraintFailed;
            final String message;

            ResponseCheckResult(boolean contraintFailed, String message) {
                this.contraintFailed = contraintFailed;
                this.message = message;
            }

        }

        ResponseCheckResult checkResponse(Response response, FormattingResultLog log);
    }

    static class ResponseCodeCheck implements ResponseCheck {

        private final int expectedResponseCode;

        public ResponseCodeCheck(int expectedResponseCode) {
            this.expectedResponseCode = expectedResponseCode;
        }

        @Override
        public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) {

            if(expectedResponseCode != response.actualResponseCode) {
                return new ResponseCheckResult(true, response.actualResponseCode + " (expected "+expectedResponseCode+")");
            } else {
                return new ResponseCheckResult(false, "["+response.actualResponseCode + " "+response.actualResponseMessage+"]");
            }
        }
    }

    static class ResponseTimeCheck implements ResponseCheck {
        final static String TIME = "TIME ";

        private final String timeConstraint;

        private final SimpleConstraintChecker simpleConstraintChecker = new SimpleConstraintChecker();

        public ResponseTimeCheck(String timeConstraint) {
            this.timeConstraint = timeConstraint;
        }

        @Override
        public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) {

            log.debug("Checking request time [{}ms] for constraint [{}]", response.requestDurationInMs, timeConstraint);
            if(!simpleConstraintChecker.check(response.requestDurationInMs, timeConstraint)) {
                return new ResponseCheckResult(true, "time ["+response.requestDurationInMs + "ms] does not fulfil constraint ["+timeConstraint+"]");
            } else {
                return new ResponseCheckResult(false, "time ["+response.requestDurationInMs + "ms] fulfils constraint ["+timeConstraint+"]");
            }
        }
    }

    static class ResponseEntityRegExCheck implements ResponseCheck {
        final static String MATCHES = "MATCHES ";

        private final Pattern expectedResponseEntityRegEx;

        public ResponseEntityRegExCheck(Pattern expectedResponseEntityRegEx) {
            this.expectedResponseEntityRegEx = expectedResponseEntityRegEx;
        }

        @Override
        public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) {
            if(!expectedResponseEntityRegEx.matcher(response.actualResponseEntity).find()) {
                return new ResponseCheckResult(true, "response does not match ["+expectedResponseEntityRegEx+']');
            } else {
                return new ResponseCheckResult(false, "response matches ["+expectedResponseEntityRegEx+"]");
            }
        }
    }

    static class ResponseHeaderCheck implements ResponseCheck {
        final static String HEADER = "HEADER ";

        private final String headerName;
        private final String headerConstraint;

        private final SimpleConstraintChecker simpleConstraintChecker = new SimpleConstraintChecker();


        public ResponseHeaderCheck(String headerExpression) {
            String[] headerCheckBits = headerExpression.split(" +", 2);
            this.headerName = headerCheckBits[0];
            this.headerConstraint = headerCheckBits[1];
        }

        @Override
        public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) {

            List headerValues = response.actualResponseHeaders.get(headerName);
            String headerVal = headerValues!=null && !headerValues.isEmpty() ? headerValues.get(0): null;

            log.debug("Checking {} with value [{}] for constraint [{}]", headerName, headerVal, headerConstraint);
            if(!simpleConstraintChecker.check(headerVal, headerConstraint)) {
                return new ResponseCheckResult(true, "header ["+headerName+"] has value ["+headerVal+"] which does not fulfil constraint ["+headerConstraint+"]");
            } else {
                return new ResponseCheckResult(false, "header ["+headerName+"] ok");
            }
        }
    }

    static class JsonPropertyCheck implements ResponseCheck {
        final static String JSON = "JSON ";

        private final String jsonPropertyPath;
        private final String jsonPropertyConstraint;

        private final SimpleConstraintChecker simpleConstraintChecker = new SimpleConstraintChecker();


        public JsonPropertyCheck(String jsonExpression) {
            String[] jsonCheckBits = jsonExpression.split(" +", 2);
            this.jsonPropertyPath = jsonCheckBits[0];
            this.jsonPropertyConstraint = jsonCheckBits[1];
        }

        @Override
        public ResponseCheckResult checkResponse(Response response, FormattingResultLog log) {

            JSONParser jsonParser;
            try {
                jsonParser = new JSONParser(response.actualResponseEntity);
            } catch(Exception e) {
                return new ResponseCheckResult(true, "invalid json response (["+jsonPropertyPath+"] cannot be checked agains constraint ["+jsonPropertyConstraint+"])");
            }

            Object propertyVal = getJsonProperty(jsonParser, jsonPropertyPath);

            log.debug("JSON property [{}] has value [{}]", jsonPropertyPath, propertyVal);

            log.debug("Checking [{}] with value [{}] for constraint [{}]", jsonPropertyPath, propertyVal, jsonPropertyConstraint);
            if(!simpleConstraintChecker.check(propertyVal, jsonPropertyConstraint)) {
                return new ResponseCheckResult(true, "json ["+jsonPropertyPath+"] has value ["+propertyVal+"] which does not fulfil constraint ["+jsonPropertyConstraint+"]");
            } else {
                return new ResponseCheckResult(false, "json property ["+jsonPropertyPath+"] with ["+propertyVal+"] fulfils constraint ["+jsonPropertyConstraint+"]");
            }
        }

        private Object getJsonProperty(JSONParser jsonParser, String jsonPropertyPath) {
            String[] jsonPropertyPathBits = jsonPropertyPath.split("(?=\\.|\\[)");
            Object currentObject = null;
            for (int i=0; i < jsonPropertyPathBits.length; i++) {
                String jsonPropertyPathBit = jsonPropertyPathBits[i];
                String propPathForEx = StringUtils.defaultIfBlank(String.join("", Arrays.copyOfRange(jsonPropertyPathBits, 0, i)), "");
                if(jsonPropertyPathBit.startsWith("[")) {
                    int arrayIndex = Integer.parseInt(jsonPropertyPathBit.substring(1,jsonPropertyPathBit.length()-1));
                    if(currentObject==null) {
                        currentObject = jsonParser.getParsedList();
                    }
                    if(!(currentObject instanceof List)) {
                        throw new IllegalArgumentException("Path '"+propPathForEx+"' is not a json list");
                    }
                    currentObject = ((List) currentObject).get(arrayIndex);
                } else {
                    String propertyName = jsonPropertyPathBit.startsWith(".") ? jsonPropertyPathBit.substring(1) : jsonPropertyPathBit;
                    if(currentObject==null) {
                        currentObject = jsonParser.getParsed();
                    }
                    if(!(currentObject instanceof Map)) {
                        throw new IllegalArgumentException("Path '"+propPathForEx+"' is not a json object");
                    }
                    currentObject = ((Map) currentObject).get(propertyName);
                }
                if(currentObject==null && /* not last */ i+1 < jsonPropertyPathBits.length) {
                    String unevaluated = String.join("", Arrays.copyOfRange(jsonPropertyPathBits, i+1, jsonPropertyPathBits.length));
                    throw new IllegalArgumentException("Path "+propPathForEx+" is null, cannot evaluate left-over part '"+unevaluated+"'");
                }
            }
            return currentObject;
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy