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

com.cloudbees.api.cr.CloudResource Maven / Gradle / Ivy

package com.cloudbees.api.cr;

import com.cloudbees.api.oauth.OauthToken;
import org.apache.commons.io.IOUtils;
import org.codehaus.jackson.map.DeserializationConfig.Feature;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Client-side representation of a remote cloud resource.
 *
 * @author Kohsuke Kawaguchi
 */
public class CloudResource {
    /**
     * URL of the cloud resource. It is always normalized to end with '/' so that relative URL
     * against this URL can be always resolved consistently.
     */
    private final URL url;

    /**
     * The value to set into the Authorize header.
     */
    private final Credential credential;

    private volatile Set types;
    private volatile ObjectNode payload;

    public CloudResource(URL url, Credential credential) {
        String e = url.toExternalForm();
        if (!e.endsWith("/")) {
            try {
                url = new URL(e +'/');
            } catch (MalformedURLException x) {
                throw new Error(x);
            }
        }
        this.url = url;
        this.credential = credential;
    }

    /**
     * A cloud resource is identified by its URL, and to talk to it we need an OAuth access token
     */
    public static CloudResource fromOAuthToken(URL url, String oauthAccessToken) {
        return new CloudResource(url,Credential.oauth(oauthAccessToken));
    }

    public static CloudResource fromOAuthToken(URL url, OauthToken token) {
        return fromOAuthToken(url,token.accessToken);
    }

    /**
     * URL of this cloud resource.
     */
    public URL getUrl() {
        return url;
    }

    public Credential getCredential() {
        return credential;
    }

    /**
     * Creates a client-side wrapper to access this cloud resource through the specified cloud resource type.
     */
    public  T as(Class facet) throws IOException {
        if (hasType(facet))
            return coerce(facet);
        throw new ClassCastException(String.format("%s is not of type: %s. Valid types are %s", url, facet, types));
    }

    /**
     * Creates a client-side wrapper to access this cloud resource through the specified cloud resource type.
     *
     * Unlike {@link #as(Class)}, this version does not check if the cloud resource
     * actually advertises the specified cloud resource type.
     */
    public  T coerce(Class facet) throws IOException {
        try {
            return facet.cast(facet.getConstructor(CloudResource.class).newInstance(this));
        } catch (InstantiationException e) {
            throw new Error(e);
        } catch (IllegalAccessException e) {
            throw new Error(e);
        } catch (InvocationTargetException e) {
            throw new Error(e);
        } catch (NoSuchMethodException e) {
            throw new Error(e);
        }
    }

    /**
     * Does this cloud resource implement the specified type?
     */
    public  boolean hasType(Class facet) throws IOException {
        CloudResourceType a = facet.getAnnotation(CloudResourceType.class);
        if (a==null)
            throw new IllegalArgumentException(facet+" isn't annotated with @CloudResourceType");
        return hasType(a.value());
    }

    /**
     * Does this cloud resource implement the specified type?
     */
    public boolean hasType(String fullyQualifiedTypeName) throws IOException {
        if (types==null)
            retrieve();
        return types.contains(fullyQualifiedTypeName);
    }

    /**
     * Under some limited circumstances, you get a list of types of another cloud resource
     * when you get a reference to it. In such a case, use this method to inform {@link CloudResource}
     * about its types. This will save an unnecessary roundtrip.
     */
    public void setTypes(Collection types) {
        this.types = new HashSet(types);
    }

    /**
     * Retrieves the current state of the CloudResource object as JSON DOM node.
     *
     * @param force
     *      if false, cached value is used if available.
     */
    public ObjectNode retrieve(boolean force) throws IOException {
        if (force || payload==null) {
            HttpURLConnection con = connect();
            // TODO: we need to honor encoding in the Content-Type header, instead of letting Jackson guess it
            payload = (ObjectNode)MAPPER.readTree(decorateResponseStream(con.getInputStream()));
        }
        return payload;
    }

    public ObjectNode retrieve() throws IOException {
        return retrieve(false);
    }

    /**
     * Retrieves the current state of the cloud resource and data-bind it to the given object via Jackson
     */
    public  T retrieve(Class type, boolean force) throws IOException {
        return MAPPER.readValue(retrieve(force),type);
    }

    public  T retrieve(Class type) throws IOException {
        return retrieve(type,false);
    }

    /**
     * Makes a POST request with the specified request payload, then return the data-bound JSON object in the specified 'response' type.
     *
     * @param path
     *      Normally relative URL (from the URL of the cloud resource) that indicates the endpoint to POST to.
     */
    public  T post(String path, Object request, Class responseType) throws IOException {
        HttpURLConnection con = (HttpURLConnection) new URL(url,path).openConnection();
        credential.authorizeRequest(con);
        sendRequest(request, con);

        if (responseType!=null)
            return MAPPER.readValue(decorateResponseStream(con.getInputStream()),responseType);
        else {
            con.getInputStream().close();
            return null;
        }
    }

    /**
     * Makes a POST request with the specified request payload, then expect 201 status that reports the location
     * of the newly created resource.
     *
     * @param path
     *      Normally relative URL (from the URL of the cloud resource) that indicates the endpoint to POST to.
     */
    public CloudResource create(String path, Object request) throws IOException {
        URL ep = new URL(url, path);
        HttpURLConnection con = (HttpURLConnection) ep.openConnection();
        credential.authorizeRequest(con);
        sendRequest(request, con);

        decorateResponseStream(con.getInputStream());

        if (con.getResponseCode()==201) {
            String location = con.getHeaderField("Location");
            if (location==null)
                throw new IllegalStateException(ep+" reported 201 but there's no location header");
            return new CloudResource(new URL(location),credential);
        }
        throw new IllegalStateException(ep+" reported success "+con.getResponseCode()+" but expecting 201");
    }

    private void sendRequest(Object request, HttpURLConnection con) throws IOException {
        con.setRequestMethod("POST");
        con.setRequestProperty("Accept", CONTENT_TYPE);
        con.setRequestProperty("Content-Type", CONTENT_TYPE_UTF8);
        con.setDoOutput(true);
        for (String t : typesOf(request.getClass())) {
            con.addRequestProperty("X-Cloud-Resource-Type",t);
        }
        dumpRequestHeaders(con);
        MAPPER.writeValue(decorateRequestStream(con.getOutputStream()),request);
        con.getOutputStream().close();

        checkError(con);
    }

    public HttpURLConnection connect() throws IOException {
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        credential.authorizeRequest(con);
        con.setRequestProperty("Accept", CONTENT_TYPE);
        dumpRequestHeaders(con);
        checkError(con);
        List v = con.getHeaderFields().get("X-Cloud-Resource-Type");
        if (v==null)
            throw new IOException(url+" is not a cloud resource. It reported no X-Cloud-Resource-Type header");
        this.types = new HashSet(v);
        String ct = con.getHeaderField("Content-Type");
        if (ct==null)
            throw new IOException(url+" is not a cloud resource. It reported no Content-Type");
        if (!ct.startsWith(CONTENT_TYPE))
            throw new IOException(url+" is not a cloud resource. It reported Content-Type="+ct+" while we expected "+CONTENT_TYPE);
        return con;
    }

    /**
     * Checks if the server reported an error, and if so throw an exception with enough information.
     *
     * We assume {@link HttpURLConnection} follows redirects, so we don't process them here.
     */
    private void checkError(HttpURLConnection con) throws IOException {
        dumpResponseHeaders(con);
        if (con.getResponseCode()/100!=2) {
            String msg = "Failed to retrieve "+url;
            InputStream er = con.getErrorStream();
            if (er!=null) {
                msg += ": "+ IOUtils.toString(er);
            }
            throw new IOException(msg);
        }
    }

    private void dumpRequestHeaders(HttpURLConnection con) {
        if (DEBUG==null)    return;

        DEBUG.println();
        DEBUG.printf("> %s %s\n",con.getRequestMethod(),con.getURL());
        for (Entry> e : con.getRequestProperties().entrySet()) {
            String header = e.getKey();
            for (String v : e.getValue()) {
                DEBUG.printf("> %s: %s\n",header,v);
            }
        }
    }

    private OutputStream decorateRequestStream(final OutputStream os) {
        if (DEBUG==null)    return os;

        return new OutputStream() {
            @Override
            public void write(int b) throws IOException {
                DEBUG.write(b);
                os.write(b);
            }

            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                DEBUG.write(b, off, len);
                os.write(b, off, len);
            }

            @Override
            public void flush() throws IOException {
                os.flush();
            }

            @Override
            public void close() throws IOException {
                os.close();
            }
        };
    }

    private InputStream decorateResponseStream(InputStream is) throws IOException {
        if (DEBUG==null || is==null)    return is;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        IOUtils.copy(is,baos);
        DEBUG.println(baos.toString());
        return new ByteArrayInputStream(baos.toByteArray());
    }

    private void dumpResponseHeaders(HttpURLConnection con) throws IOException {
        if (DEBUG==null)    return;

        DEBUG.println();
        DEBUG.printf("< %s %s\n",con.getResponseCode(),con.getResponseMessage());
        for (Entry> e : con.getHeaderFields().entrySet()) {
            String header = e.getKey();
            if (header==null)   continue;   // a bug in HttpURLConnection?
            for (String v : e.getValue()) {
                DEBUG.printf("< %s: %s\n",header,v);
            }
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CloudResource that = (CloudResource) o;

        return url.equals(that.url);
    }

    @Override
    public int hashCode() {
        return url != null ? url.hashCode() : 0;
    }

    @Override
    public String toString() {
        return url.toString();
    }

    public static final ObjectMapper MAPPER = new ObjectMapper();
    static {
        MAPPER.configure(Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        MAPPER.configure(Feature.AUTO_DETECT_FIELDS, false);
        MAPPER.configure(Feature.AUTO_DETECT_SETTERS, false);
        MAPPER.registerModule(new CloudResourceJacksonModule());
    }

    /**
     * MIME content type of the cloud resource.
     */
    public static final String CONTENT_TYPE = "application/vnd.cloudbees.resource+json";
    public static final String CONTENT_TYPE_UTF8 = CONTENT_TYPE + ";charset=UTF-8";

    /**
     * Recursively list up all the {@link CloudResourceType} annotations on the given class
     * and return them.
     */
    public static Set typesOf(Class bean) {
        Set r = new HashSet();
        typesOf(bean,r);
        return r;
    }

    private static void typesOf(Class t, Set r) {
        if (t==null)    return;
        CloudResourceType a = t.getAnnotation(CloudResourceType.class);
        if (a!=null)
            r.add(a.value());
        for (Class i : t.getInterfaces()) {
            typesOf(i,r);
        }
        typesOf(t.getSuperclass(),r);
    }

    /**
     * Capability required to retrieve the state of {@link CloudResource}.
     */
    public static Capability READ_CAPABILITY = new Capability("https://types.cloudbees.com/resource/read");

    /**
     * If set to non-null, the request and the response details are sent to this stream
     */
    public static PrintStream DEBUG = null;
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy