Java tutorial
/* * Copyright 2017 JBoss Inc * * Licensed 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 io.apiman.test.common.echo; import io.apiman.common.util.SimpleStringUtils; import io.apiman.gateway.engine.beans.EngineErrorResponse; import io.apiman.test.common.mock.EchoResponse; import io.vertx.core.AbstractVerticle; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerOptionsConverter; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.core.net.JdkSSLEngineOptions; import io.vertx.core.net.JksOptions; import io.vertx.core.streams.WriteStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import org.apache.commons.lang3.math.NumberUtils; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; /** * <p> * Vert.x edition of Echo servlet, with view to being more amenable * to performance testing. * </p> * <p> * Can be run directly with: <tt>vertx run EchoServerVertx.java</tt> * </p> * <p> * To set port, use the property -Dio.apiman.test.common.echo.port=1234 * </p> * * @author Marc Savy {@literal <marc@rhymewithgravy.com>} */ @SuppressWarnings("nls") public class EchoServerVertx extends AbstractVerticle { private Logger log = LoggerFactory.getLogger(EchoServerVertx.class); private static ObjectMapper jsonMapper = new ObjectMapper(); static { jsonMapper.enable(SerializationFeature.INDENT_OUTPUT); } private static JAXBContext jaxbContext; static { try { jaxbContext = JAXBContext.newInstance(EngineErrorResponse.class, EchoResponse.class); } catch (JAXBException e) { throw new RuntimeException(e); } } private long counter = 0L; private int toStart = 2; @Override public void start(Future<Void> startFuture) { int port = NumberUtils.toInt(System.getProperty("io.apiman.test.common.echo.port"), 9998); HttpServerOptions httpServerOptions = getHttpServerOptions(); HttpServerOptions httpsServerOptions = getHttpsServerOptions().setSsl(true) .setKeyStoreOptions(getKeystore()).setTrustStoreOptions(getTrustStore()); // Plain HTTP server vertx.createHttpServer(httpServerOptions).requestHandler(new EchoHandler()).listen(port, result -> { if (result.succeeded()) { checkSuccess(startFuture, result); } else { startFuture.fail(result.cause()); } }); // HTTPS server vertx.createHttpServer(httpsServerOptions).requestHandler(new EchoHandler()).listen(port + 1, result -> { if (result.succeeded()) { checkSuccess(startFuture, result); } else { startFuture.fail(result.cause()); } }); log.info("*** Starting EchoServerVertx on HTTP: {0} HTTPS: {1}", port, port + 1); } private void checkSuccess(Future<Void> startFuture, AsyncResult<HttpServer> result) { toStart--; if (toStart == 0) startFuture.complete(); } private HttpServerOptions getHttpServerOptions() { return getHttpServerOptions("http"); } private HttpServerOptions getHttpsServerOptions() { return getHttpServerOptions("https"); } private HttpServerOptions getHttpServerOptions(String name) { HttpServerOptions options = new HttpServerOptions(); HttpServerOptionsConverter.fromJson(config().getJsonObject(name, new JsonObject()), options); if (JdkSSLEngineOptions.isAlpnAvailable()) { options.setUseAlpn(true); } return options; } // Writes buffered chunks directly to the response and then calls #end. private static void writeXmlAndEnd(HttpServerResponse rep, EchoResponse echo) { try (BufferOutputStream bufferOutputStream = new BufferOutputStream(500, rep)) { Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); jaxbMarshaller.marshal(echo, bufferOutputStream); } catch (JAXBException e) { e.printStackTrace(); throw new RuntimeException(e); } } // Writes buffered chunks directly to the response and then calls #end. private static void writeJsonAndEnd(HttpServerResponse rep, EchoResponse echo) { try (BufferOutputStream bufferOutputStream = new BufferOutputStream(500, rep)) { jsonMapper.writeValue(bufferOutputStream, echo); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } } private final class EchoHandler implements Handler<HttpServerRequest> { private long bodyLength = 0L; private MessageDigest sha1; public EchoHandler() { try { sha1 = MessageDigest.getInstance("SHA1"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } @Override public void handle(HttpServerRequest req) { try { req.exceptionHandler(ex -> { handleError(req.response(), ex); }); _handle(req); } catch (Exception ex) { handleError(req.response(), ex); } } private void _handle(HttpServerRequest req) { HttpServerResponse rep = req.response(); if (req.headers().contains("X-Echo-ErrorCode")) { // Check if number, if not then set to 400. int errorCode = Optional.of(req.getHeader("X-Echo-ErrorCode")).filter(NumberUtils::isNumber) .map(Integer::valueOf).orElse(400); // Get error message, else set to "" to avoid NPE. String statusMsg = Optional.ofNullable(req.getHeader("X-Echo-ErrorMessage")).orElse(""); // #end writes and flushes the response. rep.setStatusCode(errorCode).setStatusMessage(statusMsg).end(); return; } // If redirect query param set, do a 302. String query = req.query(); if (query != null && query.startsWith("redirectTo=")) { String redirectTo = query.substring(11); rep.putHeader("Location", redirectTo).setStatusCode(302).end(); return; } // Determine if explicitly needs XML (else, use JSON). boolean isXml = Optional.of(req.getHeader("Accept")) .filter(accept -> accept.contains("application/xml")) .map(accept -> !(accept.contains("application/json"))).orElse(false); // Build response EchoResponse echo = new EchoResponse(); echo.setMethod(req.method().toString()); echo.setResource(normaliseResource(req)); echo.setUri(req.path()); req.handler(body -> { sha1.update(body.getBytes()); bodyLength += body.length(); }).endHandler(end -> { // If any body was present, encode digest as Base64. if (bodyLength > 0) { echo.setBodyLength(bodyLength); echo.setBodySha1(Base64.getEncoder().encodeToString(sha1.digest())); } echo.setCounter(++counter); echo.setHeaders(multimapToMap(req.headers())); rep.putHeader("Response-Counter", echo.getCounter().toString()); rep.setChunked(true); if (isXml) { // XML rep.putHeader("Content-Type", "application/xml"); writeXmlAndEnd(rep, echo); } else { // JSON rep.putHeader("Content-Type", "application/json"); writeJsonAndEnd(rep, echo); } }); } private void handleError(HttpServerResponse rep, Throwable e) { log.error(e); if (!rep.ended()) { rep.setStatusCode(500); rep.end(); } } // IMPORTANT: This is lossy private Map<String, String> multimapToMap(MultiMap headers) { LinkedHashMap<String, String> out = new LinkedHashMap<>(); headers.forEach(pair -> out.put(pair.getKey(), pair.getValue())); return out; } private String normaliseResource(HttpServerRequest req) { if (req.query() != null) { String[] normalisedQueryString = req.query().split("&"); Arrays.sort(normalisedQueryString); return req.path() + "?" + SimpleStringUtils.join("&", normalisedQueryString); } else { return req.path(); } } } private static final class BufferOutputStream extends java.io.OutputStream { private Buffer vxBuffer; private int sizeHint; private WriteStream<Buffer> writeStream; private boolean ended = false; public BufferOutputStream(int sizeHint, WriteStream<Buffer> writeStream) { this.sizeHint = sizeHint; this.writeStream = writeStream; vxBuffer = Buffer.buffer(sizeHint); } @Override public void write(int b) throws IOException { checkFlush(1); vxBuffer.appendByte((byte) b); } @Override public void write(byte b[]) throws IOException { checkFlush(b.length); vxBuffer.appendBytes(b); } @Override public void write(byte b[], int off, int len) throws IOException { checkFlush(len); vxBuffer.appendBytes(b, off, len); } private void checkFlush(int len) { if (vxBuffer.length() + len >= sizeHint) { flush(); } } @Override public void flush() { writeStream.write(vxBuffer); vxBuffer.getByteBuf().clear(); } @Override public void close() { if (!ended) { if (vxBuffer.length() > 0) { writeStream.end(vxBuffer); } else { writeStream.end(); } ended = true; } } } private JksOptions getKeystore() { return getJksOptions("keystore", "jks/keystore.jks"); } private JksOptions getTrustStore() { return getJksOptions("trustStore", "jks/truststore.ts"); } private JksOptions getJksOptions(String key, String defaultResource) { JsonObject config = config().getJsonObject(key, new JsonObject()); JksOptions jksOptions = new JksOptions().setPassword(config.getString("password", "secret")) .setValue(getResource(config.getString("resourceName", defaultResource))); return jksOptions; } private Buffer getResource(String fPath) { ClassLoader classLoader = getClass().getClassLoader(); File file = new File(classLoader.getResource(fPath).getFile()); Buffer buff; try { buff = Buffer.buffer(Files.readAllBytes(file.toPath())); } catch (IOException e) { throw new RuntimeException(e); } return buff; } }