com.viadeo.kasper.client.KasperClient.java Source code

Java tutorial

Introduction

Here is the source code for com.viadeo.kasper.client.KasperClient.java

Source

// ----------------------------------------------------------------------------
//  This file is part of the Kasper framework.
//
//  The Kasper framework is free software: you can redistribute it and/or 
//  modify it under the terms of the GNU Lesser General Public License as 
//  published by the Free Software Foundation, either version 3 of the 
//  License, or (at your option) any later version.
//
//  Kasper framework 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 Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public License
//  along with the framework Kasper.  
//  If not, see <http://www.gnu.org/licenses/>.
// --
//  Ce fichier fait partie du framework logiciel Kasper
//
//  Ce programme est un logiciel libre ; vous pouvez le redistribuer ou le 
//  modifier suivant les termes de la GNU Lesser General Public License telle 
//  que publie par la Free Software Foundation ; soit la version 3 de la 
//  licence, soit ( votre gr) toute version ultrieure.
//
//  Ce programme est distribu dans l'espoir qu'il sera utile, mais SANS 
//  AUCUNE GARANTIE ; sans mme la garantie tacite de QUALIT MARCHANDE ou 
//  d'ADQUATION  UN BUT PARTICULIER. Consultez la GNU Lesser General Public 
//  License pour plus de dtails.
//
//  Vous devez avoir reu une copie de la GNU Lesser General Public License en 
//  mme temps que ce programme ; si ce n'est pas le cas, consultez 
//  <http://www.gnu.org/licenses>
// ----------------------------------------------------------------------------
// ============================================================================
//                 KASPER - Kasper is the treasure keeper
//    www.viadeo.com - mobile.viadeo.com - api.viadeo.com - dev.viadeo.com
//
//           Viadeo Framework for effective CQRS/DDD architecture
// ============================================================================
package com.viadeo.kasper.client;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.SetMultimap;
import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;
import com.sun.jersey.api.client.*;
import com.sun.jersey.api.client.async.TypeListener;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import com.viadeo.kasper.api.component.command.Command;
import com.viadeo.kasper.api.component.command.CommandResponse;
import com.viadeo.kasper.api.component.event.Event;
import com.viadeo.kasper.api.component.query.Query;
import com.viadeo.kasper.api.component.query.QueryResponse;
import com.viadeo.kasper.api.component.query.QueryResult;
import com.viadeo.kasper.api.context.Context;
import com.viadeo.kasper.api.exception.KasperException;
import com.viadeo.kasper.api.response.CoreReasonCode;
import com.viadeo.kasper.api.response.KasperReason;
import com.viadeo.kasper.common.exposition.HttpContextHeaders;
import com.viadeo.kasper.common.exposition.TypeAdapter;
import com.viadeo.kasper.common.exposition.query.QueryBuilder;
import com.viadeo.kasper.common.exposition.query.QueryFactory;
import com.viadeo.kasper.common.serde.ObjectMapperProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.beans.Introspector;
import java.net.URI;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.sun.jersey.api.client.ClientResponse.Status.ACCEPTED;
import static com.viadeo.kasper.common.exposition.HttpContextHeaders.*;

/**
 * <p>
 * KasperClient allows to submit commands and queries to a remote kasper
 * platform. It actually wraps all the logic of communication, errors and
 * resources location resolution.
 * </p>
 * <p>
 * Instances of <strong>KasperClient are thread safe and should be
 * reused</strong> as internally some caching is done in order to improve
 * performances (mainly to avoid java introspection overhead).
 * </p>
 *
 * <p>
 * <strong>Usage</strong>
 * </p>
 * <p>
 * KasperClient supports synchronous and asynchronous requests. Sending
 * asynchronous requests can be done by asking for a java Future or by passing a
 * {@link Callback callback} argument. For example
 * submitting a command asynchronously with a callback (we will use here a
 * client with its default configuration).
 * Command and query methods can throw KasperClientException, which are
 * unchecked exceptions in order to avoid boilerplate code.
 * </p>
 *
 * <pre>
 *      KasperClient client = new KasperClient();
 *
 *      client.sendAsync(someCommand, new ICallback&lt;ICommandResponse&gt;() {
 *          public void done(final ICommandResponse response) {
 *              // do something smart with my response
 *          }
 *      });
 *
 *      // or using a future
 *
 *      Future&lt;ICommandResponse&gt; futureCommandResponse = client.sendAsync(someCommand);
 *
 *      // do some other work while the command is being processed
 *      ...
 *
 *      // block until the response is obtained
 *      ICommandResponse commandResponse = futureCommandResponse.get();
 * </pre>
 *
 * <p>
 * Using a similar pattern you can submit a query.
 * </p>
 * <p>
 * <strong>Customization</strong>
 * </p>
 *
 * <p>
 * To customize a KasperClient instance you can use the
 * {@link KasperClientBuilder}, implementing the builder pattern in order to
 * allow a fluent and intuitive construction of KasperClient instances.
 * </p>
 *
 * <p>
 * <strong>Important notes</strong>
 * </p>
 *
 * <ul>
 * <li>Query implementations must be composed only of simple types (serialized
 * to litterals), if you need a complex query or some type used in your query is
 * not supported you should ask the team responsible of maintaining the kasper
 * platform to implement a custom
 * {@link com.viadeo.kasper.common.exposition.TypeAdapter} for that specific
 * type.</li>
 * <li>At the moment the Response to which the response should be mapped is free,
 * but take care it must match the responseing stream. This will probably change
 * in the future by making IQuery parameterized with a Response. Thus query
 * methods signature could change.</li>
 * </ul>
 */
public class KasperClient {
    private static final KasperClient DEFAULT_KASPER_CLIENT = new KasperClientBuilder().create();

    public static final int DEFAULT_TIMEOUT = 6000;

    protected final Client client;
    protected final URL commandBaseLocation;
    protected final URL queryBaseLocation;
    protected final URL eventBaseLocation;

    private final Flags flags;

    @VisibleForTesting
    protected final QueryFactory queryFactory;

    @VisibleForTesting
    protected final HttpContextSerializer contextSerializer;

    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final String version;

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

    public static final class Flags {

        private boolean usePostForQueries = false;

        // -----

        public static Flags defaults() {
            return new Flags();
        }

        public Flags importFrom(final Flags flags) {
            this.usePostForQueries = flags.usePostForQueries();
            return this;
        }

        // -----

        public Flags usePostForQueries(final boolean flag) {
            this.usePostForQueries = flag;
            return this;
        }

        public boolean usePostForQueries() {
            return this.usePostForQueries;
        }

    }

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

    protected static String version() {
        final Optional<String> version = new ManifestReader(KasperClient.class).getKasperVersion();
        if (version.isPresent()) {
            return version.get();
        }
        return "nc";
    }

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

    /**
     * Creates a new KasperClient instance using the default
     * {@link KasperClientBuilder} configuration.
     */
    public KasperClient() {
        this(DEFAULT_KASPER_CLIENT.queryFactory, DEFAULT_KASPER_CLIENT.client,
                DEFAULT_KASPER_CLIENT.commandBaseLocation, DEFAULT_KASPER_CLIENT.queryBaseLocation,
                DEFAULT_KASPER_CLIENT.eventBaseLocation, DEFAULT_KASPER_CLIENT.contextSerializer, Flags.defaults());
    }

    KasperClient(final QueryFactory queryFactory, final Client client, final URL commandBaseUrl,
            final URL queryBaseUrl, final URL eventBaseLocation, final HttpContextSerializer contextSerializer,
            final Flags flags) {

        this.client = checkNotNull(client);
        this.commandBaseLocation = checkNotNull(commandBaseUrl);
        this.queryBaseLocation = checkNotNull(queryBaseUrl);
        this.queryFactory = checkNotNull(queryFactory);
        this.eventBaseLocation = checkNotNull(eventBaseLocation);
        this.contextSerializer = checkNotNull(contextSerializer);
        this.flags = checkNotNull(flags);
        this.version = version();
    }

    KasperClient(final QueryFactory queryFactory, final Client client, final URL commandBaseUrl,
            final URL queryBaseUrl, final URL eventBaseLocation, final HttpContextSerializer contextSerializer) {
        this(queryFactory, client, commandBaseUrl, queryBaseUrl, eventBaseLocation, contextSerializer,
                Flags.defaults());
    }

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

    protected <BUILDER_OUT extends RequestBuilder<BUILDER_OUT>, BUILDER_IN extends RequestBuilder<BUILDER_OUT>> BUILDER_OUT configureBuilder(
            final Context context, final BUILDER_IN builder) {
        final BUILDER_OUT builderOut = builder.accept(MediaType.APPLICATION_JSON_TYPE)
                .type(MediaType.APPLICATION_JSON_TYPE)
                .header(HttpContextHeaders.HEADER_CLIENT_VERSION.toHeaderName(), version);

        contextSerializer.serialize(context, builderOut);

        return builderOut;
    }

    // ------------------------------------------------------------------------
    // COMMANDS
    // ------------------------------------------------------------------------

    /**
     * Sends a command and waits until a response is returned.
     *
     * @param context a related context
     * @param command to submit
     * @return the command response, indicating if the command has been processed
     *         successfully or not (in that case you can get the error message
     *         from the command).
     * @throws KasperException KasperClientException if something went wrong.
     * @see CommandResponse
     */
    public CommandResponse send(final Context context, final Command command) {
        checkNotNull(command);
        checkNotNull(context);

        final WebResource.Builder builder = configureBuilder(context,
                client.resource(resolveCommandPath(command.getClass())));

        final ClientResponse response = builder.put(ClientResponse.class, command);

        try {
            return handleCommandResponse(response);
        } finally {
            closeClientResponse(response);
        }
    }

    // --

    /**
     * Sends a command and returns immediately a future allowing to retrieve the
     * response later.
     *
     * @param context a related context
     * @param command to submit
     * @return a Future allowing to retrieve the response later.
     * @throws KasperException if something went wrong.
     * @see CommandResponse
     */
    public Future<? extends CommandResponse> sendAsync(final Context context, final Command command) {
        checkNotNull(command);
        checkNotNull(context);

        final AsyncWebResource.Builder builder = configureBuilder(context,
                client.asyncResource(resolveCommandPath(command.getClass())));

        final Future<ClientResponse> futureResponse = builder.put(ClientResponse.class, command);

        // we need to decorate the Future returned by jersey in order to handle
        // exceptions and populate according to it the command response
        return new CommandResponseFuture(this, futureResponse);
    }

    // --

    /**
     * Sends a command and returns immediately, when the response is ready the
     * callback will be called with the obtained ICommandResponse as parameter.
     *
     * @param context a related context
     * @param command  to submit
     * @param callback to call when the response is ready.
     * @throws KasperException if something went wrong.
     * @see CommandResponse
     */
    public void sendAsync(final Context context, final Command command, final Callback<CommandResponse> callback) {
        checkNotNull(command);
        checkNotNull(context);

        final AsyncWebResource.Builder builder = configureBuilder(context,
                client.asyncResource(resolveCommandPath(command.getClass())));

        builder.put(new TypeListener<ClientResponse>(ClientResponse.class) {
            @Override
            public void onComplete(final Future<ClientResponse> f) throws InterruptedException {
                try {

                    ClientResponse clientResponse = f.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
                    try {
                        callback.done(handleCommandResponse(clientResponse));
                    } finally {
                        closeClientResponse(clientResponse);
                    }
                } catch (final ExecutionException e) {
                    throw new KasperException(String.format("ERROR handling command [%s]", command.getClass()), e);
                } catch (TimeoutException e) {
                    f.cancel(true);
                    throw new KasperException(
                            String.format("ERROR handling command [%s]. Timeout exception.", command.getClass()),
                            e);
                }
            }
        }, command);
    }

    /**
     * Interpret platform's response to a sent command
     *
     * @param clientResponse the native response
     * @return the CommandResponse deserialized object
     */
    CommandResponse handleCommandResponse(final ClientResponse clientResponse) {
        if (checkNotNull(clientResponse).getType().isCompatible(MediaType.APPLICATION_JSON_TYPE)) {

            final CommandResponse response = clientResponse.getEntity(CommandResponse.class);

            /* Extract security token if it has been set in headers */
            final MultivaluedMap<String, String> headers = clientResponse.getHeaders();

            if (headers.containsKey(HEADER_SECURITY_TOKEN.toHeaderName())) {
                response.withSecurityToken(headers.getFirst(HEADER_SECURITY_TOKEN.toHeaderName()));
            }
            if (headers.containsKey(HEADER_ACCESS_TOKEN.toHeaderName())) {
                response.withAccessToken(headers.getFirst(HEADER_ACCESS_TOKEN.toHeaderName()));
            }
            if (headers.containsKey(HEADER_AUTHENTICATION_TOKEN.toHeaderName())) {
                response.withAuthenticationToken(headers.getFirst(HEADER_AUTHENTICATION_TOKEN.toHeaderName()));
            }

            return new HTTPCommandResponse(safeStatusFromCode(clientResponse.getStatus()), response);

        } else {

            return new HTTPCommandResponse(safeStatusFromCode(clientResponse.getStatus()),
                    CommandResponse.Status.ERROR, new KasperReason(CoreReasonCode.UNKNOWN_REASON,
                            "Response from platform uses an unsupported type: " + clientResponse.getType()));
        }
    }

    // ------------------------------------------------------------------------
    // EVENTS
    // ------------------------------------------------------------------------

    /**
     * Sends an event and waits until a response is returned.
     *
     * @param context a related context
     * @param event to submit
     * @throws KasperException if something went wrong.
     */
    public void emit(final Context context, final Event event) {
        checkNotNull(event);
        checkNotNull(context);

        final WebResource.Builder builder = configureBuilder(context,
                client.resource(resolveEventPath(event.getClass())));

        ClientResponse response = null;

        try {
            response = builder.put(ClientResponse.class, event);
            final ClientResponse.Status status = response.getClientResponseStatus();

            if (!ACCEPTED.equals(status)) {
                throw new KasperException("event submission failed with status <" + status.getReasonPhrase() + ">");
            }

        } catch (final Exception e) {
            throw new KasperException("Unable to send event : " + event.getClass().getName(), e);
        } finally {
            closeClientResponse(response);
        }

    }

    // ------------------------------------------------------------------------
    // QUERIES
    // ------------------------------------------------------------------------

    /**
     * Send a query and maps the result to a Response.
     *
     * @param context a related context
     * @param query to submit.
     * @param <P> the type of result.
     * @param mapTo Response class to which we want to map the response.
     * @return an instance of the Response for this query.
     * @throws KasperException if something went wrong.
     */
    public <P extends QueryResult> QueryResponse<P> query(final Context context, final Query query,
            final Class<P> mapTo) {
        return query(context, query, TypeToken.of(mapTo));
    }

    /**
     * Send a query and maps the response to a Response. Here we use guavas
     * TypeToken allowing to define a generic type. This is useful if you want
     * to map the response to a IQueryCollectionResponse.
     * <p>
     * Type tokens are used like that:
     *
     * <pre>
     * SomeCollectionResponse&lt;SomeResponse&gt; someResponseCollection = client.query(someQuery,
     *         new TypeToken&lt;SomeCollectionResponse&lt;SomeResponse&gt;&gt;());
     * </pre>
     *
     * <p>
     * If you are not familiar with the concept of TypeTokens you can read <a
     * href="http://gafter.blogspot.fr/2006/12/super-type-tokens.html">this blog
     * post</a> who explains a bit more in details what it is about.
     *
     *
     * @param context a related context
     * @param query to submit.
     * @param mapTo Response class to which we want to map the response.
     * @param <P> the type of the <code>QueryResult</code>
     * @return an instance of the Response for this query.
     * @throws KasperException if something went wrong.
     */
    public <P extends QueryResult> QueryResponse<P> query(final Context context, final Query query,
            final TypeToken<P> mapTo) {
        checkNotNull(query);
        checkNotNull(mapTo);
        checkNotNull(context);

        WebResource webResource = client.resource(resolveQueryPath(query.getClass()));
        if (!flags.usePostForQueries()) {
            webResource = webResource.queryParams(prepareQueryParams(query));
        }

        final WebResource.Builder builder = configureBuilder(context, webResource);

        final ClientResponse response;
        if (flags.usePostForQueries()) {
            response = builder.post(ClientResponse.class, queryToJson(query));
        } else {
            response = builder.get(ClientResponse.class);
        }

        try {
            return handleQueryResponse(response, mapTo);
        } finally {
            closeClientResponse(response);
        }
    }

    // --

    public <P extends QueryResult> Future<QueryResponse<P>> queryAsync(final Context context, final Query query,
            final Class<P> mapTo) {
        return queryAsync(context, query, TypeToken.of(mapTo));
    }

    /**
     * FIXME should we also handle async in the platform side ?? Is it really
     * useful?
     *
     * @param context a related context
     * @param query to submit.
     * @param mapTo Response class to which we want to map the response.
     * @param <P> the type of the <code>QueryResult</code>
     * @return a future of the Response for this query.
     *
     * @see KasperClient#query(Context, com.viadeo.kasper.api.component.query.Query, Class)
     * @see KasperClient#sendAsync(Context, com.viadeo.kasper.api.component.command.Command)
     */
    public <P extends QueryResult> Future<QueryResponse<P>> queryAsync(final Context context, final Query query,
            final TypeToken<P> mapTo) {
        checkNotNull(query);
        checkNotNull(mapTo);
        checkNotNull(context);

        AsyncWebResource asyncWebResource = client.asyncResource(resolveQueryPath(query.getClass()));
        if (!flags.usePostForQueries()) {
            asyncWebResource = asyncWebResource.queryParams(prepareQueryParams(query));
        }

        final AsyncWebResource.Builder builder = configureBuilder(context, asyncWebResource);

        final Future<ClientResponse> futureResponse;

        if (flags.usePostForQueries()) {
            futureResponse = builder.post(ClientResponse.class, queryToJson(query));
        } else {
            futureResponse = builder.get(ClientResponse.class);
        }

        return new QueryResponseFuture<P>(this, futureResponse, mapTo);
    }

    // --

    /**
     * @param context a related context
     * @param query to submit.
     * @param mapTo Response class to which we want to map the response.
     * @param callback a callback
     * @param <P> the type of the <code>QueryResult</code>
     *
     * @see KasperClient#query(Context, com.viadeo.kasper.api.component.query.Query, Class)
     * @see KasperClient#sendAsync(Context, com.viadeo.kasper.api.component.command.Command, Callback)
     */
    public <P extends QueryResult> void queryAsync(final Context context, final Query query, final Class<P> mapTo,
            final Callback<QueryResponse<P>> callback) {
        queryAsync(context, query, TypeToken.of(mapTo), callback);
    }

    /**
     * @param context a related context
     * @param query to submit.
     * @param mapTo TypeToken to which we want to map the response.
     * @param callback a callback
     * @param <P> the type of the <code>QueryResult</code>
     *
     * @see KasperClient#query(Context, com.viadeo.kasper.api.component.query.Query, Class)
     * @see KasperClient#sendAsync(Context, com.viadeo.kasper.api.component.command.Command,
     *      Callback)
     */
    public <P extends QueryResult> void queryAsync(final Context context, final Query query,
            final TypeToken<P> mapTo, final Callback<QueryResponse<P>> callback) {
        checkNotNull(query);
        checkNotNull(mapTo);
        checkNotNull(context);
        checkNotNull(callback);

        AsyncWebResource asyncWebResource = client.asyncResource(resolveQueryPath(query.getClass()));
        if (!flags.usePostForQueries()) {
            asyncWebResource = asyncWebResource.queryParams(prepareQueryParams(query));
        }

        final AsyncWebResource.Builder builder = configureBuilder(context, asyncWebResource);

        final TypeListener<ClientResponse> typeListener = createTypeListener(query, mapTo, callback);

        if (flags.usePostForQueries()) {
            builder.post(typeListener, query);
        } else {
            builder.get(typeListener);
        }
    }

    private <P extends QueryResult> TypeListener<ClientResponse> createTypeListener(final Query query,
            final TypeToken<P> mapTo, final Callback<QueryResponse<P>> callback) {
        return new TypeListener<ClientResponse>(ClientResponse.class) {
            @Override
            public void onComplete(final Future<ClientResponse> f) throws InterruptedException {
                ClientResponse clientResponse = null;
                try {
                    clientResponse = f.get(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
                    callback.done(handleQueryResponse(clientResponse, mapTo));
                } catch (final ExecutionException e) {
                    throw new KasperException("ERROR handling query[" + query.getClass() + "]", e);
                } catch (TimeoutException e) {
                    f.cancel(true);
                    throw new KasperException("ERROR handling query[" + query.getClass() + "]. Timeout exception",
                            e);
                } finally {
                    closeClientResponse(clientResponse);
                }
            }
        };
    }

    <P extends QueryResult> QueryResponse<P> handleQueryResponse(final ClientResponse clientResponse,
            final TypeToken<P> mapTo) {

        if (checkNotNull(clientResponse).getType().isCompatible(MediaType.APPLICATION_JSON_TYPE)) {

            final TypeToken mappedType = new TypeToken<QueryResponse<P>>() {
            }.where(new TypeParameter<P>() {
            }, checkNotNull(mapTo));

            final QueryResponse<P> response = clientResponse
                    .getEntity(new GenericType<QueryResponse<P>>(mappedType.getType()));

            return new HTTPQueryResponse<P>(safeStatusFromCode(clientResponse.getStatus()), response);

        } else {

            return new HTTPQueryResponse<P>(safeStatusFromCode(clientResponse.getStatus()),
                    new KasperReason(CoreReasonCode.UNKNOWN_REASON,
                            "Response from platform uses an unsupported type: " + clientResponse.getType()));

        }
    }

    // --

    MultivaluedMap<String, String> prepareQueryParams(final Query query) {
        checkNotNull(query);

        final MultivaluedMap<String, String> map = new MultivaluedMapImpl();

        if (!flags.usePostForQueries()) {
            for (final Map.Entry<String, String> entry : queryToSetMap(query).entries()) {
                map.add(entry.getKey(), entry.getValue());
            }
        }

        return map;
    }

    private String queryToJson(final Query query) {
        final ObjectWriter objectWriter = ObjectMapperProvider.INSTANCE.objectWriter();
        final String queryToJson;

        try {
            queryToJson = objectWriter.writeValueAsString(query);
        } catch (final JsonProcessingException e) {
            throw new KasperException(String.format("ERROR generating query string for [%s]", query.getClass()), e);
        }

        return queryToJson;
    }

    private SetMultimap<String, String> queryToSetMap(final Query query) {
        checkNotNull(query);

        @SuppressWarnings("unchecked")
        final TypeAdapter<Query> adapter = (TypeAdapter<Query>) queryFactory.create(TypeToken.of(query.getClass()));

        final QueryBuilder queryBuilder = new QueryBuilder();
        try {

            adapter.adapt(query, queryBuilder);

        } catch (final Exception ex) {

            throw new KasperException(String.format("ERROR generating query string for [%s]", query.getClass()),
                    ex);

        }

        return queryBuilder.build();
    }

    // ------------------------------------------------------------------------
    // RESOLVERS
    // ------------------------------------------------------------------------

    protected URI resolveCommandPath(final Class<? extends Command> commandClass) {
        final String className = commandClass.getSimpleName().replace("Command", "");
        return resolvePath(commandBaseLocation, Introspector.decapitalize(className), commandClass);
    }

    protected URI resolveQueryPath(final Class<? extends Query> queryClass) {
        final String className = queryClass.getSimpleName().replace("Query", "");
        return resolvePath(queryBaseLocation, Introspector.decapitalize(className), queryClass);
    }

    protected URI resolveEventPath(final Class<? extends Event> eventClass) {
        final String className = eventClass.getSimpleName().replace("Event", "");
        return resolvePath(eventBaseLocation, Introspector.decapitalize(className), eventClass);
    }

    private URI resolvePath(final URL basePath, final String path, final Class clazz) {
        try {

            return new URL(basePath, path).toURI();

        } catch (final Exception e) {
            throw cannotConstructURI(clazz, e);
        }
    }

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

    private KasperException cannotConstructURI(final Class clazz, final Exception e) {
        return new KasperException("Could not construct resource url for " + clazz, e);
    }

    private Response.Status safeStatusFromCode(final int code) {
        final Response.Status status = Response.Status.fromStatusCode(code);
        if (null == status) {
            return Response.Status.INTERNAL_SERVER_ERROR;
        } else {
            return status;
        }
    }

    void closeClientResponse(ClientResponse clientResponse) {
        if (clientResponse != null) {
            try {
                clientResponse.close();
            } catch (Throwable t) {
                logger.error("Can't close client response", t);
            }
        }
    }
}