ste.xtest.net.StubURLConnection.java Source code

Java tutorial

Introduction

Here is the source code for ste.xtest.net.StubURLConnection.java

Source

/*
 * xTest
 * Copyright (C) 2016 Stefano Fornari
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY Stefano Fornari, Stefano Fornari
 * DISCLAIMS THE WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 */
package ste.xtest.net;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.SerializationUtils;
import org.assertj.core.util.Lists;
import ste.xtest.logging.LoggingByteArrayOutputStream;
import ste.xtest.net.calls.ErrorThrower;

/**
 *
 * @author ste
 * 
 * @TODO: getInputStream shall return a correct input stream accordingly to the 
 *        content type
 */
public class StubURLConnection extends HttpURLConnection implements Cloneable {

    public StubURLConnection(URL url) {
        super(url);

        status = HttpURLConnection.HTTP_OK;
        headers = new HashMap<>();
        connected = false;
    }

    /**
     * Stubs the connection action to the resource. It also executes the 
     * provided <code>StubConnectionCall</code> if any.
     * 
     * @throws IOException in case of connection errors
     * @throws IllegalStateException if already connected
     */
    @Override
    public void connect() throws IOException {
        if (connected) {
            throw new IllegalStateException("Already connected");
        }

        Logger LOG = Logger.getLogger("ste.xtest.net");
        if (LOG.isLoggable(Level.INFO)) {
            LOG.info("connecting to " + url);
            LOG.info("request headers: " + getRequestProperties());
            LOG.info("response headers: " + headers);
        }

        if (exec != null) {
            try {
                LOG.info("executing connection code");
                exec.call(this);
            } catch (IOException x) {
                throw x;
            } catch (Exception x) {
                throw new IOException(x.getMessage(), x);
            }
        }

        connected = true;
    }

    @Override
    public void disconnect() {
        connected = false;
    }

    @Override
    public boolean usingProxy() {
        return false;
    }

    @Override
    public int getResponseCode() throws IOException {
        //
        // Like in <code>java.net.HttpURLConnection</code>, ensure that we 
        // have connected to the server calling getInputStream()
        //
        getInputStream();
        return getStatus();
    }

    @Override
    public String getResponseMessage() {
        return getMessage();
    }

    @Override
    public Object getContent() throws IOException {
        connectIfNeeded();
        return content;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        connectIfNeeded();

        if (content == null) {
            return null;
        }

        Logger LOG = Logger.getLogger("ste.xtest.net");

        if (content instanceof String) {
            if (LOG.isLoggable(Level.INFO)) {
                LOG.info("returning input stream from provided text");
            }
            return new ByteArrayInputStream(((String) content).getBytes());
        } else if (content instanceof Path) {
            if (LOG.isLoggable(Level.INFO)) {
                LOG.info("returning input stream from file " + ((Path) content).toAbsolutePath());
            }
            return Files.newInputStream((Path) content);
        } else if (content instanceof InputStream) {
            if (LOG.isLoggable(Level.INFO)) {
                LOG.info("returning input stream from reader");
            }
            return ((InputStream) content);
        }

        if (LOG.isLoggable(Level.INFO)) {
            LOG.info("returning input stream from provided data");
        }
        return new ByteArrayInputStream((byte[]) content);
    }

    /**
     * Returns the resource output stream.
     * 
     * @return the connection output stream
     */
    @Override
    public OutputStream getOutputStream() throws IOException {
        if (out == null) {
            //
            // create the output stream
            //
            Logger LOG = Logger.getLogger("ste.xtest.net");

            Level level = LOG.getLevel();
            out = new LoggingByteArrayOutputStream(LOG, (level == null) ? Level.INFO : level, 2500);
        }

        return out;
    }

    @Override
    public InputStream getErrorStream() {
        try {
            return getInputStream();
        } catch (IOException x) {
            return null;
        }
    }

    @Override
    public String getHeaderField(final String key) {
        List<String> values = getHeaders().get(key);

        if ((values != null) && (values.size() > 0)) {
            return values.get(values.size() - 1);
        }

        return null;
    }

    @Override
    public Map<String, List<String>> getHeaderFields() {
        Map<String, List<String>> copy = new HashMap<>();

        for (String key : getHeaders().keySet()) {
            copy.put(key, Collections.unmodifiableList(getHeaders().get(key)));
        }
        return Collections.unmodifiableMap(copy);
    }

    @Override
    public void setRequestMethod(String m) throws ProtocolException {
        super.setRequestMethod(m);
    }

    // --------------------------------------------------------------- Cloneable

    @Override
    public Object clone() {
        try {
            URL clonedURL = new URL(getURL().toString());
            StubURLConnection C = new StubURLConnection(clonedURL);

            if (message != null) {
                C.message(new String(message));
            }

            C.status(status);

            //
            // we need to preserve the content type if changed (setting content
            // sets also the type)
            //
            String originalType = getContentType();
            if (content != null) {

                if (content instanceof byte[]) {
                    byte[] original = (byte[]) content;
                    byte[] clonedContent = new byte[original.length];
                    System.arraycopy(original, 0, clonedContent, 0, original.length);
                    C.content(clonedContent);
                } else if (content instanceof String) {
                    C.text(new String((String) content));
                } else if (content instanceof Path) {
                    C.file(new String(((Path) content).toString()));
                }
            }
            C.type(originalType);

            C.headers(SerializationUtils.clone(headers));
            C.exec(exec);

            return C;
        } catch (MalformedURLException x) {
            //
            // This should never happen because the URL is sanitized when given
            // to the constructor
            //
            throw new IllegalStateException("unexpected malformed url " + getURL());
        } catch (Throwable x) {
            x.printStackTrace();
            throw x;
        }
    }

    // -------------------------------------------------------------------------

    private int status;
    private String message;
    private Object content;
    private HashMap<String, List<String>> headers; // TO BE REMOVED IN FAVOUR OF SUPERCLASS' FIELD
    private StubConnectionCall exec;
    private LoggingByteArrayOutputStream out; // this will not be cloned

    /**
     * Sets the HTTP(s) status
     * 
     * @param status a valid HTTP status
     * 
     * @return this
     */
    public StubURLConnection status(int status) {
        this.status = status;
        return this;
    }

    /**
     * Store the provided response message
     * 
     * @param message the response message - MY BE NULL
     * 
     * @return this
     */
    public StubURLConnection message(final String message) {
        this.message = message;
        return this;
    }

    /**
     * Sets the content type of the request. Note that it replaces the current 
     * value if set (e.g. by calling content(), text() html()).
     * 
     * @param type - the content type - NOT BLANK
     * 
     * @return this builder
     */
    public StubURLConnection type(final String type) {
        if (type == null) {
            headers.remove("content-type");
        } else {
            headers.put("content-type", Lists.newArrayList(type));
        }

        return this;
    }

    /**
     * Sets the body, content-type (application/octet-stream) and content-length
     * (the length of content or 0 if content is null) of the request. 
     * 
     * @param content - MAY BE NULL
     * 
     * @return this builder
     */
    public StubURLConnection content(final byte[] content) {
        setContent(content, "application/octet-stream");
        return this;
    }

    /**
     * Sets the body, content-type (text/plain) and content-length (the length 
     * text or 0 if text is null) of the request. 
     * 
     * @param text - MAY BE NULL
     * 
     * @return this builder
     */
    public StubURLConnection text(final String text) {
        setContent(text, "text/plain");
        return this;
    }

    /**
     * Sets the body, content-type (text/html) and content-length (the length 
     * of html or 0 if text is null) of the request. 
     * 
     * @param html - MAY BE NULL
     * 
     * @return this
     */
    public StubURLConnection html(final String html) {
        setContent(html, "text/html");
        return this;
    }

    /**
     * Sets the body, content-type (application/json) and content-length (the 
     * length of json or 0 if json is null) of the request. 
     * 
     * @param json - MAY BE NULL
     * 
     * @return this
     */
    public StubURLConnection json(final String json) {
        setContent(json, "application/json");
        return this;
    }

    /**
     * Sets the body, content-type (depending on file) and content-length (-1 if
     * the file does not exist or the file length if the file exists) of the 
     * request. 
     * 
     * @param file - MAY BE NULL
     * 
     * @return this
     */
    public StubURLConnection file(final String file) {
        String type = null;

        Path path = (file == null) ? null : FileSystems.getDefault().getPath(file);
        if (path != null) {
            try {
                type = Files.probeContentType(path);
            } catch (IOException x) {
                //
                // noting to do
                //
            }
        }

        setContent(path, (type == null) ? "application/octet-stream" : type);
        return this;
    }

    /**
     * Stores the given header.
     * 
     * @param header header name - MUST NOT BE EMPTY
     * @param values header values - MAY BE NULL
     * 
     * @return this
     */
    public StubURLConnection header(final String header, final String... values) {
        headers.put(header, Lists.newArrayList(values));
        return this;
    }

    /**
     * Stores the given headers
     * 
     * @param headers the headers map - MAY BE NULL
     * 
     * @return this
     */
    public StubURLConnection headers(final HashMap<String, List<String>> headers) {
        this.headers = headers;
        return this;
    }

    /**
     * Tells the stub to throw the given error on connection. This is a shortcut
     * to use <code>exec()</code> and throw the desired exception. A side effect
     * i sthat <code>error()</code> and <code>exec()</code> should not use 
     * together (each overrides the other).
     * 
     * @param error the error to rise
     * 
     * @return this
     */
    public StubURLConnection error(final IOException error) {
        exec = (error == null) ? null : new ErrorThrower(error);

        return this;
    }

    /**
     * A Callable that will be executed upon connection. This procedure can 
     * perform any action on content, headers or status of the request and is
     * intended to add a bit of intelligence when the stub is used. It may be 
     * used to check conditions or change the status or the content based on
     * some criteria.
     * 
     * If the execution of the Callable throws an exception a IOException will
     * be thrown unless overridden by <code>error()</code>.
     * 
     * Note that <code>StubConnectionCall</code> will not be cloned, which means
     * that all clone will use the same value.
     * 
     * @param exec the parameter task to call upon connection - MAY BE NULL
     * 
     * @return this
     * 
     * 
     */
    public StubURLConnection exec(final StubConnectionCall exec) {
        this.exec = exec;
        return this;
    }

    public int getStatus() {
        return status;
    }

    public String getMessage() {
        return message;
    }

    public Map<String, List<String>> getHeaders() {
        return headers;
    }

    public boolean isConnected() {
        return connected;
    }

    // --------------------------------------------------------- private methods

    private void setContent(final Object content, final String type) {
        this.content = content;
        headers.put("content-type", Lists.newArrayList(type));
        headers.put("content-length", Lists.newArrayList(getContentLength(content)));
    }

    private String getContentLength(final Object content) {
        long len = -1;

        if (content == null) {
            len = 0;
        } else {
            if (content instanceof byte[]) {
                len = ((byte[]) content).length;
            } else if (content instanceof String) {
                len = ((String) content).length();
            } else if (content instanceof Path) {
                try {
                    len = Files.size((Path) content);
                } catch (IOException x) {
                    //
                    // nothing to do, it will take -1
                    //
                }
            }
        }

        return String.valueOf(len);
    }

    private void connectIfNeeded() throws IOException {
        if (!isConnected()) {
            connect();
        }
    }
}