/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2012-2017 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.jersey.test.inmemory; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.security.Principal; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.ws.rs.ProcessingException; import javax.ws.rs.client.Client; import javax.ws.rs.core.Configuration; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.client.spi.ConnectorProvider; import org.glassfish.jersey.internal.MapPropertiesDelegate; import org.glassfish.jersey.internal.PropertiesDelegate; import org.glassfish.jersey.message.internal.OutboundMessageContext; import org.glassfish.jersey.server.ApplicationHandler; import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.ContainerResponse; import org.glassfish.jersey.server.spi.ContainerResponseWriter; /** * In-memory client connector. * * @author Pavel Bucek (pavel.bucek at oracle.com) * @author Marek Potociar (marek.potociar at oracle.com) */ class InMemoryConnector implements Connector { private static final Logger LOGGER = Logger.getLogger(InMemoryConnector.class.getName()); private final URI baseUri; private final ApplicationHandler appHandler; /** * In-memory client connector provider. */ static class Provider implements ConnectorProvider { private final URI baseUri; private final ApplicationHandler appHandler; /** * Create new in-memory connector provider. * * @param baseUri application base URI. * @param appHandler RequestInvoker instance which represents application. */ Provider(URI baseUri, ApplicationHandler appHandler) { this.baseUri = baseUri; this.appHandler = appHandler; } @Override public Connector getConnector(Client client, Configuration config) { return new InMemoryConnector(baseUri, appHandler); } } /** * Create new in-memory connector. * * @param baseUri application base URI. * @param appHandler RequestInvoker instance which represents application. */ private InMemoryConnector(final URI baseUri, final ApplicationHandler appHandler) { this.baseUri = baseUri; this.appHandler = appHandler; } /** * In memory container response writer. */ public static class InMemoryResponseWriter implements ContainerResponseWriter { private MultivaluedMap<String, String> headers; private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); private boolean committed; private Response.StatusType statusInfo; @Override public OutputStream writeResponseStatusAndHeaders(long contentLength, ContainerResponse responseContext) { List<Object> length = new ArrayList<>(); length.add(String.valueOf(contentLength)); responseContext.getHeaders().put(HttpHeaders.CONTENT_LENGTH, length); headers = responseContext.getStringHeaders(); statusInfo = responseContext.getStatusInfo(); return baos; } @Override public boolean suspend(long timeOut, TimeUnit timeUnit, TimeoutHandler timeoutHandler) { LOGGER.warning("Asynchronous server side invocations are not supported by InMemoryContainer."); return false; } @Override public void setSuspendTimeout(long timeOut, TimeUnit timeUnit) { throw new UnsupportedOperationException("Async server side invocations are not supported by InMemoryContainer."); } @Override public void commit() { committed = true; } @Override public void failure(Throwable error) { throw new ProcessingException("Server-side request processing failed with an error.", error); } @Override public boolean enableResponseBuffering() { return true; } /** * Get the written entity. * * @return Byte array which contains the entity written by the server. */ public byte[] getEntity() { if (!committed) { throw new IllegalStateException("Response is not committed yet."); } return baos.toByteArray(); } /** * Return response headers. * * @return headers. */ public MultivaluedMap<String, String> getHeaders() { return headers; } /** * Returns response status info. * * @return status info. */ public Response.StatusType getStatusInfo() { return statusInfo; } } /** * {@inheritDoc} * <p/> * Transforms client-side request to server-side and invokes it on provided application ({@link ApplicationHandler} * instance). * * @param clientRequest client side request to be invoked. */ @Override public ClientResponse apply(final ClientRequest clientRequest) { PropertiesDelegate propertiesDelegate = new MapPropertiesDelegate(); final ContainerRequest containerRequest = new ContainerRequest(baseUri, clientRequest.getUri(), clientRequest.getMethod(), null, propertiesDelegate); containerRequest.getHeaders().putAll(clientRequest.getStringHeaders()); final ByteArrayOutputStream clientOutput = new ByteArrayOutputStream(); if (clientRequest.getEntity() != null) { clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() { @Override public OutputStream getOutputStream(int contentLength) throws IOException { final MultivaluedMap<String, Object> clientHeaders = clientRequest.getHeaders(); if (contentLength != -1 && !clientHeaders.containsKey(HttpHeaders.CONTENT_LENGTH)) { containerRequest.getHeaders().putSingle(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength)); } return clientOutput; } }); clientRequest.enableBuffering(); try { clientRequest.writeEntity(); } catch (IOException e) { final String msg = "Error while writing entity to the output stream."; LOGGER.log(Level.SEVERE, msg, e); throw new ProcessingException(msg, e); } } containerRequest.setEntityStream(new ByteArrayInputStream(clientOutput.toByteArray())); boolean followRedirects = ClientProperties.getValue(clientRequest.getConfiguration().getProperties(), ClientProperties.FOLLOW_REDIRECTS, true); final InMemoryResponseWriter inMemoryResponseWriter = new InMemoryResponseWriter(); containerRequest.setWriter(inMemoryResponseWriter); containerRequest.setSecurityContext(new SecurityContext() { @Override public Principal getUserPrincipal() { return null; } @Override public boolean isUserInRole(String role) { return false; } @Override public boolean isSecure() { return false; } @Override public String getAuthenticationScheme() { return null; } }); appHandler.handle(containerRequest); return tryFollowRedirects(followRedirects, createClientResponse( clientRequest, inMemoryResponseWriter), new ClientRequest(clientRequest)); } @Override public Future<?> apply(final ClientRequest request, final AsyncConnectorCallback callback) { CompletableFuture<ClientResponse> future = new CompletableFuture<>(); try { ClientResponse response = apply(request); callback.response(response); future.complete(response); } catch (ProcessingException ex) { future.completeExceptionally(ex); } catch (Throwable t) { callback.failure(t); future.completeExceptionally(t); } return future; } @Override public void close() { // do nothing } private ClientResponse createClientResponse(final ClientRequest clientRequest, final InMemoryResponseWriter responseWriter) { final ClientResponse clientResponse = new ClientResponse(responseWriter.getStatusInfo(), clientRequest); clientResponse.getHeaders().putAll(responseWriter.getHeaders()); clientResponse.setEntityStream(new ByteArrayInputStream(responseWriter.getEntity())); return clientResponse; } @SuppressWarnings("MagicNumber") private ClientResponse tryFollowRedirects(boolean followRedirects, ClientResponse response, ClientRequest request) { if (!followRedirects) { return response; } while (true) { switch (response.getStatus()) { case 303: case 302: case 307: request = new ClientRequest(request); request.setUri(response.getLocation()); if (response.getStatus() == 303) { request.setMethod("GET"); } response = apply(request); break; default: return response; } } } @Override public String getName() { return "Jersey InMemory Connector"; } }