Author: ghenzler
Date: Thu Jan 10 23:53:02 2019
New Revision: 1850992

URL: http://svn.apache.org/viewvc?rev=1850992&view=rev
Log:
FELIX-6024 Generic check HttpRequestsCheck

Added:
    
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java
   (with props)
    
felix/trunk/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java
   (with props)
Modified:
    felix/trunk/healthcheck/docs/felix-health-checks.md
    felix/trunk/healthcheck/generalchecks/bnd.bnd
    felix/trunk/healthcheck/generalchecks/pom.xml
    
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java

Modified: felix/trunk/healthcheck/docs/felix-health-checks.md
URL: 
http://svn.apache.org/viewvc/felix/trunk/healthcheck/docs/felix-health-checks.md?rev=1850992&r1=1850991&r2=1850992&view=diff
==============================================================================
--- felix/trunk/healthcheck/docs/felix-health-checks.md (original)
+++ felix/trunk/healthcheck/docs/felix-health-checks.md Thu Jan 10 23:53:02 2019
@@ -123,7 +123,8 @@ Memory | org.apache.felix.hc.generalchec
 CPU | org.apache.felix.hc.generalchecks.CpuCheck | no | Checks for CPU usage - 
`cpuPercentageThresholdWarn` (default 95%) can be set to control what CPU usage 
produces status `WARN` (check never results in `CRITICAL`)
 Thread Usage | org.apache.felix.hc.generalchecks.ThreadUsageCheck | no | 
Checks via `ThreadMXBean.findDeadlockedThreads()` for deadlocks and analyses 
the CPU usage of each thread via a configurable time period (`samplePeriodInMs` 
defaults to 200ms). Uses `cpuPercentageThresholdWarn` (default 95%) to `WARN` 
about high thread utilisation.   
 Bundles Started | org.apache.felix.hc.generalchecks.BundlesStartedCheck | yes 
| Checks for started bundles - `includesRegex` and `excludesRegex` control what 
bundles are checked. 
-JMX Attribute Check | org.apache.felix.hc.generalchecks.JmxAttributeCheckk | 
yes | Allows to check an arbitrary JMX attribute (using the configured mbean 
`mbean.name`'s attribute `attribute.name`) against a given constraint 
`attribute.value.constraint`. Can check multiple attributes by providing 
additional config properties with numbers:  `mbean2.name`' `attribute2.name` 
and `attribute2.value.constraint`.
+JMX Attribute Check | org.apache.felix.hc.generalchecks.JmxAttributeCheck | 
yes | Allows to check an arbitrary JMX attribute (using the configured mbean 
`mbean.name`'s attribute `attribute.name`) against a given constraint 
`attribute.value.constraint`. Can check multiple attributes by providing 
additional config properties with numbers:  `mbean2.name`' `attribute2.name` 
and `attribute2.value.constraint`.
+HttpRequestsCheck | org.apache.felix.hc.generalchecks.HttpRequestsCheck | yes 
| Allows to check a list of URLs against response code, response headers, 
timing, response content (plain content via RegEx or JSON via path expression).
 
 
 ## Executing Health Checks

Modified: felix/trunk/healthcheck/generalchecks/bnd.bnd
URL: 
http://svn.apache.org/viewvc/felix/trunk/healthcheck/generalchecks/bnd.bnd?rev=1850992&r1=1850991&r2=1850992&view=diff
==============================================================================
--- felix/trunk/healthcheck/generalchecks/bnd.bnd (original)
+++ felix/trunk/healthcheck/generalchecks/bnd.bnd Thu Jan 10 23:53:02 2019
@@ -7,3 +7,5 @@ Bundle-DocURL: https://felix.apache.org
 Bundle-License: Apache License, Version 2.0
 
 Bundle-Vendor: The Apache Software Foundation
+
+Conditional-Package: org.apache.commons.cli.*,org.apache.felix.utils.*

Modified: felix/trunk/healthcheck/generalchecks/pom.xml
URL: 
http://svn.apache.org/viewvc/felix/trunk/healthcheck/generalchecks/pom.xml?rev=1850992&r1=1850991&r2=1850992&view=diff
==============================================================================
--- felix/trunk/healthcheck/generalchecks/pom.xml (original)
+++ felix/trunk/healthcheck/generalchecks/pom.xml Thu Jan 10 23:53:02 2019
@@ -37,6 +37,10 @@
     <description>
         General purpose health checks that can be simply configured to check 
for generic aspects like disk space, started bundles, remote service 
availability, etc.
     </description>
+    
+    <properties>
+        <felix.java.version>8</felix.java.version>
+    </properties>
 
     <scm>
         
<connection>scm:svn:http://svn.apache.org/repos/asf/felix/trunk/healthcheck/genericchecks</connection>
@@ -106,7 +110,6 @@
             <scope>provided</scope>
         </dependency>
 
-
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
@@ -120,7 +123,22 @@
             <version>3.4</version>
             <scope>provided</scope>
         </dependency>
-   
+        
+        <!--  START provided via ConditionalPackage -->
+        <dependency>
+            <groupId>commons-cli</groupId>
+            <artifactId>commons-cli</artifactId>
+            <version>1.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.utils</artifactId>
+            <version>1.11.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <!--  END provided via ConditionalPackage -->
+
         <!-- START test scope dependencies -->
         <dependency>
             <groupId>junit</groupId>

Added: 
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java
URL: 
http://svn.apache.org/viewvc/felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java?rev=1850992&view=auto
==============================================================================
--- 
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java
 (added)
+++ 
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java
 Thu Jan 10 23:53:02 2019
@@ -0,0 +1,631 @@
+/*
+ * 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.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.commons.lang3.StringUtils;
+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.generalchecks.util.SimpleConstraintChecker;
+import org.apache.felix.utils.json.JSONParser;
+import org.osgi.framework.BundleContext;
+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.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)
+@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 <RegEx>' 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 <title>html title.*</title>",
+            "/path/example.html => 200 && MATCHES <title>html title.*</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<RequestSpec> requestSpecs;
+    private int connectTimeoutInMs;
+    private int readTimeoutInMs;
+    private Result.Status statusForFailedContraint;
+    private boolean runInParallel;
+    
+    private String defaultBaseUrl = null;
+
+    private FormattingResultLog configErrors;
+    
+    @Activate
+    protected void activate(BundleContext bundleContext, 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(bundleContext);
+        
+        LOG.info("Default BaseURL: {}", defaultBaseUrl);
+        LOG.info("Activated Requests HC: {}", requestSpecs);
+    }
+
+    private void setupDefaultBaseUrl(BundleContext bundleContext) {
+        ServiceReference<?> serviceReference = 
bundleContext.getServiceReference("org.osgi.service.http.HttpService");
+        boolean isHttp = 
Boolean.parseBoolean(String.valueOf(serviceReference.getProperty("org.apache.felix.http.enable")));
+        boolean isHttps = 
Boolean.parseBoolean(String.valueOf(serviceReference.getProperty("org.apache.felix.https.enable")));
+        if(isHttp) {
+            defaultBaseUrl = 
"http://localhost:"+serviceReference.getProperty("org.osgi.service.http.port");
+        } else if(isHttps) {
+            defaultBaseUrl = 
"http://localhost:"+serviceReference.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<RequestSpec> requestSpecsStream = runInParallel ? 
requestSpecs.parallelStream() : requestSpecs.stream();
+        List<FormattingResultLog> 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<RequestSpec> getRequestSpecs(String[] requestSpecStrArr) {
+        
+        configErrors = new FormattingResultLog();
+        
+        List<RequestSpec> requestSpecs = new ArrayList<RequestSpec>();
+        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<String,String> headers = new HashMap<String,String>();
+        String data = null;
+        
+        String user;
+        
+        Integer connectTimeoutInMs;
+        Integer readTimeoutInMs;
+        
+        Proxy proxy;
+        
+        List<ResponseCheck> responseChecks = new ArrayList<ResponseCheck>();
+  
+        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(StringUtils.isNumeric(clause)) {
+                    responseChecks.add(new 
ResponseCodeCheck(Integer.parseInt(clause)));
+                } else if(StringUtils.startsWithIgnoreCase(clause, 
ResponseTimeCheck.TIME)) {
+                    responseChecks.add(new 
ResponseTimeCheck(clause.substring(ResponseTimeCheck.TIME.length())));
+                } else if(StringUtils.startsWithIgnoreCase(clause, 
ResponseEntityRegExCheck.MATCHES)) {
+                    responseChecks.add(new 
ResponseEntityRegExCheck(Pattern.compile(clause.substring(ResponseEntityRegExCheck.MATCHES.length()))));
+                } else if(StringUtils.startsWithIgnoreCase(clause, 
ResponseHeaderCheck.HEADER)) {
+                    responseChecks.add(new 
ResponseHeaderCheck(clause.substring(ResponseHeaderCheck.HEADER.length())));
+                } else if(StringUtils.startsWithIgnoreCase(clause, 
JsonPropertyCheck.JSON)) {
+                    responseChecks.add(new 
JsonPropertyCheck(clause.substring(JsonPropertyCheck.JSON.length())));
+                } else {
+                    throw new IllegalArgumentException("Invalid response 
content assertion clause: '"+clause+"'");
+                }
+            }
+        }
+
+        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<String> argList = new ArrayList<String>();
+            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<String> resultBits = new ArrayList<String>();
+                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+": "+ 
StringUtils.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<String,String> 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<String, List<String>> 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<String, List<String>> actualResponseHeaders;
+        final String actualResponseEntity;
+        final long requestDurationInMs;
+        
+        public Response(int actualResponseCode, String actualResponseMessage, 
Map<String, List<String>> 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;
+        }
+
+        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;
+        }
+
+        public ResponseCheckResult checkResponse(Response response, 
FormattingResultLog log) {
+
+            log.debug("Checking request time [{}ms] for constraint [{}]", 
response.requestDurationInMs, timeConstraint);
+            if(!simpleConstraintChecker.check((Long) 
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;
+        }
+        
+        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];
+        }
+        
+        public ResponseCheckResult checkResponse(Response response, 
FormattingResultLog log) {
+
+            List<String> 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];
+        }
+        
+        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];
+                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 
'"+StringUtils.defaultIfEmpty(StringUtils.join(jsonPropertyPathBits, "", 0, i), 
"<root>")+"' 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 
'"+StringUtils.defaultIfEmpty(StringUtils.join(jsonPropertyPathBits, "", 0, i), 
"<root>")+"' is not a json object");
+                    }
+                    currentObject = ((Map<?,?>) 
currentObject).get(propertyName);
+                }
+                if(currentObject==null && /* not last */ i+1 < 
jsonPropertyPathBits.length) {
+                    throw new IllegalArgumentException("Path 
"+StringUtils.join(jsonPropertyPathBits, "", 0, i+1)+" is null, cannot evaluate 
left-over part '"+StringUtils.join(jsonPropertyPathBits, "", i+1, 
jsonPropertyPathBits.length)+"'");
+                }
+            }
+            return currentObject;
+        }
+    }
+    
+}

Propchange: 
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/HttpRequestsCheck.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: 
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java
URL: 
http://svn.apache.org/viewvc/felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java?rev=1850992&r1=1850991&r2=1850992&view=diff
==============================================================================
--- 
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java
 (original)
+++ 
felix/trunk/healthcheck/generalchecks/src/main/java/org/apache/felix/hc/generalchecks/util/SimpleConstraintChecker.java
 Thu Jan 10 23:53:02 2019
@@ -70,9 +70,12 @@ public class SimpleConstraintChecker {
             matches = value < Long.valueOf(parts[1]);
 
         } else if (parts[0].equals(EQUALS) && parts.length == 2) {
-            long value = Long.valueOf(stringValue).longValue();
-            matches = value == Long.valueOf(parts[1]).longValue();
-
+            if(StringUtils.isNumeric(stringValue)) {
+                long value = Long.valueOf(stringValue).longValue();
+                matches = value == Long.valueOf(parts[1]).longValue();
+            } else {
+                matches = stringValue.equals(parts[1]);
+            }
         } else if (parts.length == 4 && BETWEEN.equalsIgnoreCase(parts[0]) && 
AND.equalsIgnoreCase(parts[2])) {
             long value = Long.valueOf(stringValue).longValue();
             long lowerBound = Long.valueOf(parts[1]).longValue();

Added: 
felix/trunk/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java
URL: 
http://svn.apache.org/viewvc/felix/trunk/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java?rev=1850992&view=auto
==============================================================================
--- 
felix/trunk/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java
 (added)
+++ 
felix/trunk/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java
 Thu Jan 10 23:53:02 2019
@@ -0,0 +1,160 @@
+/*
+ * 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 org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doReturn;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+import org.apache.felix.hc.api.FormattingResultLog;
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog.Entry;
+import org.apache.felix.hc.generalchecks.HttpRequestsCheck.RequestSpec;
+import 
org.apache.felix.hc.generalchecks.HttpRequestsCheck.ResponseCheck.ResponseCheckResult;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class HttpRequestsCheckTest {
+
+    FormattingResultLog log = new FormattingResultLog();
+
+    HttpRequestsCheck.Response simple200HtmlResponse = new 
HttpRequestsCheck.Response(200, "OK", null, 
"<html><head><title>test</title></head><body>body text</body></html>", 200);
+
+    @Test
+    public void testRequestSpecParsing() throws Exception {
+        
+        HttpRequestsCheck.RequestSpec requestSpec = new 
HttpRequestsCheck.RequestSpec("/path/to/page.html");
+        assertEquals("/path/to/page.html", requestSpec.url);
+        assertEquals("GET", requestSpec.method);
+        assertEquals(new HashMap<String,String>(), requestSpec.headers);
+        assertNull(requestSpec.data);
+        assertNull(requestSpec.user);
+        assertNull(requestSpec.connectTimeoutInMs);
+        assertNull(requestSpec.readTimeoutInMs);
+        assertNull(requestSpec.proxy);
+        
+        requestSpec = new HttpRequestsCheck.RequestSpec("-X POST -H \"X-Test: 
Test\" -d \"{ 1,2,3 }\" -u admin:admin --connect-timeout 4 -m 5 --proxy 
http://proxy:2000 /path/to/page.html => 201");
+        assertEquals("/path/to/page.html", requestSpec.url);
+        assertEquals("POST", requestSpec.method);
+        HashMap<String, String> expectedHeaders = new HashMap<String,String>();
+        expectedHeaders.put("X-Test", "Test");
+        expectedHeaders.put("Authorization", "Basic YWRtaW46YWRtaW4=");
+        assertEquals(expectedHeaders, requestSpec.headers);
+        assertEquals("{ 1,2,3 }", requestSpec.data);
+        assertEquals("admin", requestSpec.user);
+        assertEquals((Integer) 4000, requestSpec.connectTimeoutInMs);
+        assertEquals((Integer) 5000, requestSpec.readTimeoutInMs);
+        assertEquals("proxy:2000", requestSpec.proxy.address().toString());
+
+    }
+    
+    @Test
+    public void testSimpleRequestSpec() throws Exception {
+
+        HttpRequestsCheck.RequestSpec requestSpec = new 
HttpRequestsCheck.RequestSpec("/path/to/page.html");
+        Entry entry = fakeRequestForSpecAndReturnResponse(requestSpec, 
simple200HtmlResponse);
+        assertEquals(Result.Status.OK, entry.getStatus());
+
+        requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html => 
200");
+        entry = fakeRequestForSpecAndReturnResponse(requestSpec, 
simple200HtmlResponse);
+        assertEquals(Result.Status.OK, entry.getStatus());
+
+        requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html => 
401");
+        entry = fakeRequestForSpecAndReturnResponse(requestSpec, 
simple200HtmlResponse);
+        assertEquals(Result.Status.WARN, entry.getStatus());
+        assertThat(entry.getMessage(), containsString("200 (expected 401)"));
+    }
+    
+    @Test
+    public void testSimpleRequestSpecWithContentCheck() throws Exception {
+
+        HttpRequestsCheck.RequestSpec requestSpec = new 
HttpRequestsCheck.RequestSpec("/path/to/page.html => 200 && MATCHES 
(body|other) text");
+        Entry entry = fakeRequestForSpecAndReturnResponse(requestSpec, 
simple200HtmlResponse);
+        assertEquals(Result.Status.OK, entry.getStatus());
+        assertThat(entry.getMessage(), containsString("[200 OK], response 
matches [(body|other) text]"));
+
+        requestSpec = new HttpRequestsCheck.RequestSpec("/path/to/page.html => 
200 && MATCHES special text");
+        entry = fakeRequestForSpecAndReturnResponse(requestSpec, 
simple200HtmlResponse);
+        assertEquals(Result.Status.WARN, entry.getStatus());
+        assertThat(entry.getMessage(), containsString("[200 OK], response does 
not match [special text]"));
+
+    }
+
+    private Entry 
fakeRequestForSpecAndReturnResponse(HttpRequestsCheck.RequestSpec 
requestSpecOrig, HttpRequestsCheck.Response response) throws Exception {
+        RequestSpec requestSpec = Mockito.spy(requestSpecOrig);
+        doReturn(response).when(requestSpec).performRequest(anyString(), 
anyString(), anyInt(), anyInt(), any(FormattingResultLog.class));
+        FormattingResultLog resultLog = 
requestSpec.check("http://localhost:8080";, 10000, 10000, Result.Status.WARN, 
true);
+        Iterator<Entry> entryIt = resultLog.iterator();
+        Entry lastEntry = null;
+        while(entryIt.hasNext()) {
+            lastEntry = entryIt.next();
+        }
+        return lastEntry;
+    }
+
+    
+    @Test
+    public void testJsonConstraint() {
+
+        String testJson = "{\"test\": { \"intProp\": 2, \"arrProp\": 
[\"test1\",\"test2\",\"test3\",{\"deepProp\": \"deepVal\"}]} }";
+
+        assertJsonResponse(testJson, "test.intProp = 2", true);
+        assertJsonResponse(testJson, "test.arrProp[2] = test3", true);
+        assertJsonResponse(testJson, "test.intProp between 1 and 3", true);
+        assertJsonResponse(testJson, "test.arrProp[3].deepProp matches 
deep.*", true);
+    }
+
+    private void assertJsonResponse(String testJson, String jsonExpression, 
boolean expectedTrueOrFalse) {
+        HttpRequestsCheck.JsonPropertyCheck jsonPropertyCheck = new 
HttpRequestsCheck.JsonPropertyCheck(jsonExpression);
+        HttpRequestsCheck.Response response = new 
HttpRequestsCheck.Response(200, "OK", null, testJson, 200);
+        ResponseCheckResult checkResult = 
jsonPropertyCheck.checkResponse(response, log);
+        assertEquals("Expected "+expectedTrueOrFalse + " for expression 
["+jsonExpression+"] against json: "+testJson, expectedTrueOrFalse, 
!checkResult.contraintFailed);
+    }
+    
+    @Test
+    public void testTimeConstraint() {
+        assertTimeConstraint(1000, "< 2000", true);
+        assertTimeConstraint(1000, "between 500 and 2000", true);
+    }
+    
+    private void assertTimeConstraint(long time, String constraint, boolean 
expectedTrueOrFalse) {
+        HttpRequestsCheck.ResponseTimeCheck responseTimeCheck = new 
HttpRequestsCheck.ResponseTimeCheck(constraint);
+        HttpRequestsCheck.Response response = new 
HttpRequestsCheck.Response(200, "OK", null, "", time);
+        ResponseCheckResult checkResult = 
responseTimeCheck.checkResponse(response, log);
+        assertEquals("Expected "+expectedTrueOrFalse + " for expression 
["+constraint+"] against json: "+time+"ms", expectedTrueOrFalse, 
!checkResult.contraintFailed);
+    }
+    
+    @Test
+    public void testSplitArgsRespectingQuotes() throws Exception {
+    
+        HttpRequestsCheck.RequestSpec requestSpec = new 
HttpRequestsCheck.RequestSpec("/page.html");
+        String[] args = requestSpec.splitArgsRespectingQuotes("normal1 \"one 
two three\" normal2 'one two three' -p --words \"w1 w2 w3\"");
+        assertArrayEquals(new String[] {"normal1", "\"one two three\"", 
"normal2", "'one two three'", "-p", "--words", "\"w1 w2 w3\""}, args);
+    }
+
+
+}

Propchange: 
felix/trunk/healthcheck/generalchecks/src/test/java/org/apache/felix/hc/generalchecks/HttpRequestsCheckTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain


Reply via email to