eu.trentorise.opendata.jackan.ckan.CkanClient.java Source code

Java tutorial

Introduction

Here is the source code for eu.trentorise.opendata.jackan.ckan.CkanClient.java

Source

/* 
 * Copyright 2015 Trento Rise  (trentorise.eu) 
 *
 * 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 eu.trentorise.opendata.jackan.ckan;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableList;

import eu.trentorise.opendata.jackan.JackanException;
import eu.trentorise.opendata.jackan.SearchResults;
import eu.trentorise.opendata.commons.OdtUtils;
import static eu.trentorise.opendata.commons.OdtUtils.checkNotEmpty;
import static eu.trentorise.opendata.commons.OdtUtils.removeTrailingSlash;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;

import org.apache.http.HttpHost;
import org.apache.http.client.fluent.Content;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;
import org.apache.http.entity.ContentType;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

/**
 * Client to access a ckan instance. Threadsafe.
 *
 * The client is a thin wrapper upon Ckan api, thus one method call should
 * correspond to only one web api call. This means that sometimes to get a full
 * object from Ckan, you will need to do a second call (for example, calling
 * getDataset will also return its resources because Ckan sends them with the
 * dataset, but to be sure to have all the fields of a resource you will need to
 * call getResource).
 *
 * @author David Leoni, Ivan Tankoyeu
 *
 */
public class CkanClient {

    /**
     * CKAN uses UTC timezone, and doesn't append 'Z' to dates.
     */
    public static final String CKAN_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS";
    /**
     * Sometimes we get back Python "None" as a string instead of proper JSON
     * null
     */
    public static final String NONE = "None";

    public static final DateTimeFormatter ckanDateFormatter = DateTimeFormat.forPattern(CKAN_DATE_PATTERN);

    @Nullable
    private static ObjectMapper objectMapper;

    /**
     * Notice that even for the same api version (at least for versions <= 3)
     * different CKAN instances can behave quite differently (sic)
     */
    public static final ImmutableList<Integer> SUPPORTED_API_VERSIONS = ImmutableList.of(3);

    private final String catalogURL;

    @Nullable
    private final String ckanToken;

    private static final Logger logger = Logger.getLogger(CkanClient.class.getName());

    @Nullable
    private HttpHost proxy = null;

    /**
     * @return a clone of the json object mapper used internally.
     */
    public static ObjectMapper getObjectMapperClone() {
        return getObjectMapper().copy();
    }

    /**
     * Retrieves the Jackson object mapper. Internally, Object mapper is
     * initialized at first call.
     */
    static ObjectMapper getObjectMapper() {
        if (objectMapper == null) {
            objectMapper = new ObjectMapper();
            objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES)
                    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // let's be tolerant
                    .configure(MapperFeature.USE_GETTERS_AS_SETTERS, false) // not good for unmodifiable collections, if we will ever use any
                    .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

            // When reading dates, Jackson defaults to using GMT for all processing unless specifically told otherwise, see http://wiki.fasterxml.com/JacksonFAQDateHandling
            // When writing dates, Jackson would add a Z for timezone by CKAN doesn't use it, i.e.  "2013-11-11T04:12:11.110868"                            so we removed it here
            objectMapper.setDateFormat(new SimpleDateFormat(CKAN_DATE_PATTERN)); // but this only works for Java Dates...

            // ...so taken solution from here: http://www.lorrin.org/blog/2013/06/28/custom-joda-time-dateformatter-in-jackson/
            objectMapper.registerModule(new JodaModule());
            objectMapper.registerModule(new GuavaModule());

            objectMapper.registerModule(new SimpleModule() {
                {
                    addSerializer(DateTime.class, new StdSerializer<DateTime>(DateTime.class) {
                        @Override
                        public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider)
                                throws IOException {
                            jgen.writeString(ckanDateFormatter.print(value));
                        }

                    });

                    addDeserializer(DateTime.class, CkanDateDeserializer.forType(DateTime.class));
                }
            });

        }
        return objectMapper;
    }

    /**
     * Creates a Ckan client with null token and proxy
     *
     * @param url the catalog url i.e. http://data.gov.uk
     */
    public CkanClient(String url) {
        this(url, null, null);
    }

    /**
     * Creates a Ckan client with null proxy.
     *
     * @param URL the catalog url i.e. http://data.gov.uk. Internally, it will
     * be stored in a normalized format (to avoid i.e. trailing slashes).
     * @param token the private token string for ckan repository
     */
    public CkanClient(String URL, @Nullable String token) {
        this(URL, token, null);
    }

    /**
     * Creates a Ckan client.
     *
     * @param URL the catalog url i.e. http://data.gov.uk. Internally, it will
     * be stored in a normalized format (to avoid i.e. trailing slashes).
     * @param token the private token string for ckan repository
     * @param proxy the proxy used to perform GET and POST calls
     */
    public CkanClient(String URL, @Nullable String token, @Nullable HttpHost proxy) {
        checkNotEmpty(URL, "invalid ckan catalog url");
        this.catalogURL = removeTrailingSlash(URL);
        this.ckanToken = token;
        this.proxy = proxy;
    }

    @Override
    public String toString() {
        return "CkanClient{" + "catalogURL=" + catalogURL + ", ckanToken=" + ckanToken + '}';
    }

    /**
     * Calculates a full url out of the provided params
     *
     * @param path something like /api/3/package_show
     * @param params list of key, value parameters. They must be not be url
     * encoded. i.e. "id","laghi-monitorati-trento"
     * @return the full url to be called.
     * @throws JackanException if there is any error building the url
     */
    private String calcFullUrl(String path, Object[] params) {
        checkNotNull(path);

        try {
            StringBuilder sb = new StringBuilder().append(catalogURL).append(path);
            for (int i = 0; i < params.length; i += 2) {
                sb.append(i == 0 ? "?" : "&").append(URLEncoder.encode(params[i].toString(), "UTF-8")).append("=")
                        .append(URLEncoder.encode(params[i + 1].toString(), "UTF-8"));
            }
            return sb.toString();
        } catch (Exception ex) {
            throw new JackanException("Error while building url to perform GET! \n path: " + path + " \n params: "
                    + Arrays.toString(params), ex);
        }
    }

    /**
     * Method for http GET
     *
     * @param <T>
     * @param responseType a descendant of CkanResponse
     * @param path something like /api/3/package_show
     * @param params list of key, value parameters. They must be not be url
     * encoded. i.e. "id","laghi-monitorati-trento"
     * @throws JackanException on error
     */
    <T extends CkanResponse> T getHttp(Class<T> responseType, String path, Object... params) {
        checkNotNull(responseType);
        checkNotNull(path);

        String fullUrl = calcFullUrl(path, params);

        try {
            logger.log(Level.FINE, "getting {0}", fullUrl);
            Request request = Request.Get(fullUrl);
            if (proxy != null) {
                request.viaProxy(proxy);
            }
            String json = request.execute().returnContent().asString();
            T dr = getObjectMapper().readValue(json, responseType);
            if (!dr.success) {
                // monkey patching error type
                throw new JackanException("Reading from catalog " + catalogURL + " was not successful. Reason: "
                        + CkanError.read(getObjectMapper().readTree(json).get("error").asText()));
            }
            return dr;
        } catch (Exception ex) {
            throw new JackanException("Error while performing GET. Request url was: " + fullUrl, ex);
        }
    }

    /**
     *
     * @param <T>
     * @param responseType a descendant of CkanResponse
     * @param path something like 1/api/3/action/package_create
     * @param body the body of the POST
     * @param the content type, i.e.
     * @param params list of key, value parameters. They must be not be url
     * encoded. i.e. "id","laghi-monitorati-trento"
     * @throws JackanException on error
     */
    <T extends CkanResponse> T postHttp(Class<T> responseType, String path, String body, ContentType contentType,
            Object... params) {
        checkNotNull(responseType);
        checkNotNull(path);
        checkNotNull(body);
        checkNotNull(contentType);

        String fullUrl = calcFullUrl(path, params);

        try {

            logger.log(Level.FINE, "posting to url {0}", fullUrl);
            Request request = Request.Post(fullUrl);
            if (proxy != null) {
                request.viaProxy(proxy);
            }
            Response response = request.bodyString(body, contentType).addHeader("Authorization", ckanToken)
                    .execute();

            Content out = response.returnContent();
            String json = out.asString();

            T dr = getObjectMapper().readValue(json, responseType);
            if (!dr.success) {
                // monkey patching error type
                throw new JackanException("posting to catalog " + catalogURL + " was not successful. Reason: "
                        + CkanError.read(getObjectMapper().readTree(json).get("error").asText()));
            }
            return dr;
        } catch (Exception ex) {
            throw new JackanException("Error while performing a POST! Request url is:" + fullUrl, ex);
        }

    }

    /**
     * Returns the catalog URL (normalized).
     */
    public String getCatalogURL() {
        return catalogURL;
    }

    /**
     * Returns the private CKAN token.
     */
    public String getCkanToken() {
        return ckanToken;
    }

    public static DateTime parseRevisionTimestamp(String revisionTimestamp) {
        checkNotEmpty(revisionTimestamp, "invalid ckan revision timestamp");
        return DateTime.parse(revisionTimestamp, ISODateTimeFormat.dateHourMinuteSecond());
    }

    /**
     * Given some dataset parameters, reconstruct the URL of dataset page in the
     * catalog website.
     *
     * Valid URLs have this format with the name:
     * http://dati.trentino.it/dataset/impianti-di-risalita-vivifiemme-2013
     *
     * @param datasetIdentifier either of name the dataset (preferred) or the
     * alphanumerical id.
     *
     * @param catalogUrl i.e. http://dati.trentino.it
     */
    public static String makeDatasetURL(String catalogUrl, String datasetIdentifier) {
        checkNotEmpty(catalogUrl, "invalid catalog url");
        checkNotEmpty(datasetIdentifier, "invalid dataset Identifier");
        return removeTrailingSlash(catalogUrl) + "/dataset/" + datasetIdentifier;
    }

    /**
     *
     * Given some resource parameters, reconstruct the URL of resource page in
     * the catalog website.
     *
     * Valid URLs have this format with the dataset name
     * 'impianti-di-risalita-vivifiemme-2013':
     * http://dati.trentino.it/dataset/impianti-di-risalita-vivifiemme-2013/resource/779d1d9d-9370-47f4-a194-1b0328c32f02
     *
     * @param catalogUrl i.e. http://dati.trentino.it
     * @param datasetIdentifier the dataset name (preferred) or the
     * alphanumerical id
     *
     * @param resourceId the alphanumerical id of the resource (DON'T use
     * resource name)
     */
    public static String makeResourceURL(String catalogUrl, String datasetIdentifier, String resourceId) {
        checkNotEmpty(catalogUrl, "invalid catalog url");
        checkNotEmpty(datasetIdentifier, "invalid dataset identifier");
        checkNotEmpty(resourceId, "invalid resource id");
        return OdtUtils.removeTrailingSlash(catalogUrl) + "/" + datasetIdentifier + "/resource/" + resourceId;
    }

    /**
     *
     * Given some group parameters, reconstruct the URL of group page in the
     * catalog website.
     *
     * Valid URLs have this format with the group name
     * 'gestione-del-territorio':
     *
     * http://dati.trentino.it/group/gestione-del-territorio
     *
     * @param catalogUrl i.e. http://dati.trentino.it
     * @param groupId the group name as in {@link CkanGroup#getName()}
     * (preferred), or the group's alphanumerical id.
     */
    public static String makeGroupURL(String catalogUrl, String groupId) {
        checkNotEmpty(catalogUrl, "invalid catalog url");
        checkNotEmpty(groupId, "invalid dataset identifier");
        return OdtUtils.removeTrailingSlash(catalogUrl) + "/group/" + groupId;
    }

    /**
     * @return list of strings like i.e. limestone-pavement-orders
     * @throws JackanException on error
     */
    public synchronized List<String> getDatasetList() {
        return getHttp(DatasetListResponse.class, "/api/3/action/package_list").result;
    }

    /**
     *
     * @param limit
     * @param offset Starts with 0 included. getDatasetList(1,0) will return
     * exactly one dataset, if catalog is not empty.
     * @return list of data names like i.e. limestone-pavement-orders
     * @throws JackanException on error
     */
    public synchronized List<String> getDatasetList(Integer limit, Integer offset) {
        return getHttp(DatasetListResponse.class, "/api/3/action/package_list", "limit", limit, "offset",
                offset).result;
    }

    /**
     * Returns the list of available licenses in the ckan catalog.
     */
    public synchronized List<CkanLicense> getLicenseList() {
        return getHttp(LicenseListResponse.class, "/api/3/action/license_list").result;
    }

    /**
     * Returns the latest api version supported by the catalog
     *
     * @throws JackanException on error
     */
    public synchronized int getApiVersion() {
        for (int i = 3; i >= 1; i--) {
            return getApiVersion(i); // this is demential. But /api always gives { "version": 1} ....
        }
        throw new JackanException("Error while getting api version!", this);
    }

    /**
     * Returns the latest api version supported by the catalog
     *
     * @throws JackanException on error
     */
    private synchronized int getApiVersion(int number) {
        String fullUrl = catalogURL + "/api/" + number;
        logger.log(Level.FINE, "getting {0}", fullUrl);
        try {
            Request request = Request.Get(fullUrl);
            if (proxy != null) {
                request.viaProxy(proxy);
            }
            String json = request.execute().returnContent().asString();
            return getObjectMapper().readValue(json, ApiVersionResponse.class).version;
        } catch (Exception ex) {
            throw new JackanException("Error while fetching api version!", this, ex);
        }

    }

    /**
     * @param idOrName either the dataset name (i.e. laghi-monitorati-trento) or
     * the the alphanumerical id (i.e. 96b8aae4e211f3e5a70cdbcbb722264256ae2e7d)
     *
     * @throws JackanException on error
     */
    public synchronized CkanDataset getDataset(String idOrName) {
        CkanDataset cd = getHttp(DatasetResponse.class, "/api/3/action/package_show", "id", idOrName).result;
        for (CkanResource cr : cd.getResources()) {
            cr.setPackageId(cd.getId());
        }
        return cd;
    }

    /**
     * @throws JackanException on error
     */
    public synchronized List<CkanUser> getUserList() {
        return getHttp(UserListResponse.class, "/api/3/action/user_list").result;
    }

    /**
     * @param id i.e. 'admin'
     * @throws JackanException on error
     */
    public synchronized CkanUser getUser(String id) {
        return getHttp(UserResponse.class, "/api/3/action/user_show", "id", id).result;
    }

    /**
     * @param id The alphanumerical id of the resource, such as
     * d0892ada-b8b9-43b6-81b9-47a86be126db.
     *
     * @throws JackanException on error
     */
    public synchronized CkanResource getResource(String id) {
        return getHttp(ResourceResponse.class, "/api/3/action/resource_show", "id", id).result;
    }

    /**
     * Returns the groups present in Ckan.
     *
     * Notice that organizations will <i>not</i> be returned by this method. To
     * get them, use {@link #getOrganizationList() } instead.
     *
     * @throws JackanException on error
     */
    public synchronized List<CkanGroup> getGroupList() {
        return getHttp(GroupListResponse.class, "/api/3/action/group_list", "all_fields", "True").result;
    }

    /**
     * @throws JackanException on error
     */
    public synchronized List<String> getGroupNames() {
        return getHttp(GroupNamesResponse.class, "/api/3/action/group_List").result;
    }

    /**
     * Returns a Ckan group. Do not pass an organization id, to get organization
     * use {@link #getOrganization(java.lang.String) } instead.
     *
     * @param idOrName either the group name (i.e. hospitals-in-trento-district)
     * or the group alphanumerical id
     * @throws JackanException on error
     */
    public synchronized CkanGroup getGroup(String idOrName) {
        return getHttp(GroupResponse.class, "/api/3/action/group_show", "id", idOrName, "include_datasets",
                "false").result;
    }

    /**
     * Returns the organizations present in CKAN.
     *
     * @see #getGroupList()
     *
     * @throws JackanException on error
     */
    public synchronized List<CkanOrganization> getOrganizationList() {
        return getHttp(OrganizationListResponse.class, "/api/3/action/organization_list", "all_fields",
                "True").result;
    }

    /**
     * Returns all the resource formats available in the catalog.
     *
     * @throws JackanException on error
     */
    public synchronized Set<String> getFormats() {
        return getHttp(FormatListResponse.class, "/api/3/action/format_autocomplete", "q", "", "limit",
                "1000").result;
    }

    /**
     *
     * @throws JackanException on error
     */
    public synchronized List<String> getOrganizationNames() {
        return getHttp(GroupNamesResponse.class, "/api/3/action/organization_List").result;
    }

    /**
     * Returns a Ckan organization.
     *
     * @param organizationIdOrName either the name of organization (i.e.
     * culture-and-education) or the alphanumerical id (i.e.
     * 232cad97-ecf2-447d-9656-63899023887f). Do not pass it a group id.
     * @throws JackanException on error
     */
    public synchronized CkanOrganization getOrganization(String organizationIdOrName) {
        return getHttp(OrganizationResponse.class, "/api/3/action/organization_show", "id", organizationIdOrName,
                "include_datasets", "false").result;
    }

    /**
     * Returns a list of tags names, i.e. "gp-practice-earnings","Aid Project
     * Evaluation", "tourism-satellite-account". We think names SHOULD be
     * lowercase with minuses instead of spaces, but in some cases they aren't.
     *
     * @throws JackanException on error
     */
    public synchronized List<CkanTag> getTagList() {
        return getHttp(TagListResponse.class, "/api/3/action/tag_list", "all_fields", "True").result;
    }

    /**
     * Returns tags containing the string given in query.
     *
     * @param query
     * @throws JackanException on error
     */
    public synchronized List<String> getTagNamesList(String query) {
        return getHttp(TagNamesResponse.class, "/api/3/action/tag_list", "query", query).result;
    }

    /**
     * @throws JackanException on error
     */
    public synchronized List<String> getTagNamesList() {
        return getHttp(TagNamesResponse.class, "/api/3/action/tag_list").result;
    }

    /**
     * Search datasets containing provided text in the metadata
     *
     * @param text The query string
     * @param limit maximum results to return
     * @param offset search begins from offset. Starts from 0, so that offset 0
     * limit 1 returns exactly 1 result, if there is a matching dataset)
     * @throws JackanException on error
     */
    public synchronized SearchResults<CkanDataset> searchDatasets(String text, int limit, int offset) {
        return searchDatasets(CkanQuery.filter().byText(text), limit, offset);
    }

    /**
     * @param fqPrefix either "" or " AND "
     * @param list list of names of ckan objects
     */
    private static String appendNamesList(String fqPrefix, String key, List<String> list, StringBuilder fq) {
        if (list.size() > 0) {
            fq.append(fqPrefix).append("(");
            String prefix = "";
            for (String n : list) {
                fq.append(prefix).append(key).append(":");
                fq.append('"' + n + '"');
                prefix = " AND ";
            }
            fq.append(")");
            return " AND ";
        } else {
            return "";
        }

    }

    /**
     * @params s a string to encode in a format suitable for URLs.
     */
    private static String urlEncode(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8").replaceAll("\\+", "%20");
        } catch (UnsupportedEncodingException ex) {
            throw new JackanException("Unsupported encoding", ex);
        }
    }

    /**
     * Search datasets containg param text in the metadata
     *
     * @param query The query object
     * @param limit maximum results to return
     * @param offset search begins from offset
     * @throws JackanException on error
     */
    public synchronized SearchResults<CkanDataset> searchDatasets(CkanQuery query, int limit, int offset) {

        StringBuilder params = new StringBuilder();

        params.append("rows=").append(limit).append("&start=").append(offset);

        if (query.getText().length() > 0) {
            params.append("&q=");
            params.append(urlEncode(query.getText()));
        }

        StringBuilder fq = new StringBuilder();
        String fqPrefix = "";

        fqPrefix = appendNamesList(fqPrefix, "groups", query.getGroupNames(), fq);
        fqPrefix = appendNamesList(fqPrefix, "organization", query.getOrganizationNames(), fq);
        fqPrefix = appendNamesList(fqPrefix, "tags", query.getTagNames(), fq);
        fqPrefix = appendNamesList(fqPrefix, "license_id", query.getLicenseIds(), fq);

        if (fq.length() > 0) {
            params.append("&fq=").append(urlEncode(fq.insert(0, "(").append(")").toString()));
        }

        DatasetSearchResponse dsr;
        dsr = getHttp(DatasetSearchResponse.class, "/api/3/action/package_search?" + params.toString());

        if (dsr.success) {
            for (CkanDataset ds : dsr.result.getResults()) {
                for (CkanResource cr : ds.getResources()) {
                    cr.setPackageId(ds.getId());
                }
            }
        }

        return dsr.result;
    }

    /**
     * Creates ckan resource on the server.
     *
     * @param resource ckan resource object with the minimal set of parameters
     * required. See
     * {@link CkanResource#CkanResource(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)}
     * @return the newly created resource
     * @throws JackanException
     */
    public synchronized CkanResource createResource(CkanResourceMinimized resource) {
        checkResource(resource);

        if (ckanToken == null) {
            throw new JackanException(
                    "Tried to create resource" + resource.getName() + ", but ckan token was not set!");
        }

        ObjectMapper objMapper = CkanClient.getObjectMapper();
        String json = null;
        try {
            json = objMapper.writeValueAsString(resource);
        } catch (IOException e) {
            throw new JackanException("Couldn't serialize the provided CkanResource!", e);
        }
        return postHttp(ResourceResponse.class, "/api/3/action/resource_create", json,
                ContentType.APPLICATION_JSON).result;
    }

    /**
     * Creates ckan resource on the server.
     *
     * @param resource ckan resource object with the minimal set of parameters
     * required. See
     * {@link CkanResource#CkanResource(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)}
     * @return the newly created resource
     * @throws JackanException
     */
    public synchronized CkanResource createResource(CkanResource resource) {

        logger.warning("TODO 0.4 CREATION OF FULL RESOURCE IS EXPERIMENTAL!");

        if (ckanToken == null) {
            throw new JackanException(
                    "Tried to create resource" + resource.getName() + ", but ckan token was not set!");
        }

        ObjectMapper objMapper = CkanClient.getObjectMapper();
        String json = null;
        try {
            json = objMapper.writeValueAsString(resource);
        } catch (IOException e) {
            throw new JackanException("Couldn't serialize the provided CkanResource!", e);
        }
        return postHttp(ResourceResponse.class, "/api/3/action/resource_create", json,
                ContentType.APPLICATION_JSON).result;
    }

    /**
     * Checks if the provided resource meets the requirements to be created to
     * CKAN.
     *
     * @throws Throwable if requirements aren't met
     */
    private static void checkResource(CkanResourceMinimized resource) {
        checkNotNull(resource);
        checkNotNull(resource.getFormat(), "Ckan resource format must not be null!");
        checkNotEmpty(resource.getName(), "Ckan resource name can't be empty!");
        checkNotNull(resource.getDescription(), "Ckan resource description must not be null!");
        // todo do we need to check mimetype?? checkNotNull(resource.getMimetype());
        checkNotNull(resource.getPackageId(), "Ckan resource parent dataset must not be null!");
        checkNotNull(resource.getUrl(), "Ckan resource url must not be null!");
    }

    /**
     * Updates a resource on the ckan server.
     *
     * @param resource ckan resource object
     * @return the updated resource
     * @throws JackanException
     */
    public synchronized CkanResource updateResource(CkanResource resource, Boolean checkConsistency) {

        if (ckanToken == null) {
            throw new JackanException(
                    "Tried to update resource" + resource.getName() + ", but ckan token was not set!");
        }
        //check consistance with original version of the to be updated resource 
        if (checkConsistency) {
            CkanResource originalResource = getResource(resource.getId());

            for (Field f : originalResource.getClass().getDeclaredFields()) {
                f.setAccessible(true);
                String fieldName;
                try {
                    if ((f.get(originalResource) != null) && (f.get(resource) == null)
                            && (!f.getName().equals("created"))) {
                        fieldName = f.getName();
                        //                  if(!fieldName.equals("created")){
                        f.set(resource, f.get(originalResource));
                        System.out.println("Not a null: " + fieldName + " Value: ");
                        //                  //};
                        //                        System.out.println("Not a null: "+fieldName+ " Value: ");
                    }
                } catch (IllegalArgumentException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }

        }
        //System.out.println("After consistance checking resource is consist of: "+resource.toString());

        ObjectMapper objectMapper = CkanClient.getObjectMapper();
        String json = null;
        try {
            json = objectMapper.writeValueAsString(resource);
        } catch (IOException e) {
            throw new JackanException("Couldn't serialize the provided CkanResourceMinimized!", e);
        }

        return postHttp(ResourceResponse.class, "/api/3/action/resource_update", json,
                ContentType.APPLICATION_JSON).result;

    }

    /**
     * Updates a dataset on the ckan server
     *
     * @param dataset ckan dataset object with the minimal set of parameters
     * required to perform an update. (see {@link eu.trentorise.opendata.jackan.ckan.CkanDataset#CkanDataset(java.lang.String, java.lang.String, java.util.List)
     * }
     *
     * @return the updated dataset
     * @throws JackanException
     */
    public synchronized CkanDataset updateDataset(CkanDataset dataset) {
        checkNotNull(dataset);

        throw new UnsupportedOperationException("todo 0.4 implement me!");
    }

    /**
     * Creates CkanDataset on the server
     *
     * @param dataset data set with a given parameters
     * @return the newly created dataset
     * @throws JackanException
     */
    public synchronized CkanDataset createDataset(CkanDataset dataset) {

        logger.warning("TODO 0.4 CREATION OF FULL DATASETS IS EXPERIMENTAL!");

        checkNotNull(dataset);

        if (ckanToken == null) {
            throw new JackanException(
                    "Tried to create dataset" + dataset.getName() + ", but ckan token was not set!");
        }

        ObjectMapper objectMapper = CkanClient.getObjectMapper();
        String json = null;
        try {
            json = objectMapper.writeValueAsString(dataset);
        } catch (IOException e) {
            throw new JackanException("Couldn't serialize the provided CkanDatasetMinimized!", this, e);
        }
        DatasetResponse datasetresponse = postHttp(DatasetResponse.class, "/api/3/action/package_create", json,
                ContentType.APPLICATION_JSON);
        return datasetresponse.result;
    }

    /**
     * Creates CkanDataset on the server
     *
     * @param dataset data set with a given parameters
     * @return the newly created dataset
     * @throws JackanException
     */
    public synchronized CkanDataset createDataset(CkanDatasetMinimized dataset) {

        checkNotNull(dataset);

        if (ckanToken == null) {
            throw new JackanException(
                    "Tried to create dataset" + dataset.getName() + ", but ckan token was not set!");
        }

        ObjectMapper objectMapper = CkanClient.getObjectMapper();
        String json = null;
        try {
            json = objectMapper.writeValueAsString(dataset);
        } catch (IOException e) {
            throw new JackanException("Couldn't serialize the provided CkanDatasetMinimized!", this, e);
        }
        DatasetResponse datasetresponse = postHttp(DatasetResponse.class, "/api/3/action/package_create", json,
                ContentType.APPLICATION_JSON);
        return datasetresponse.result;
    }

    /**
     * Returns the proxy used by the client.
     */
    @Nullable
    public HttpHost getProxy() {
        return proxy;
    }

}

class CkanResponse {

    public String help;
    public boolean success;
    public CkanError error;

}

class DatasetResponse extends CkanResponse {

    public CkanDataset result;
}

class ResourceResponse extends CkanResponse {

    public CkanResource result;
}

class DatasetListResponse extends CkanResponse {

    public List<String> result;
}

class UserListResponse extends CkanResponse {

    public List<CkanUser> result;
}

class UserResponse extends CkanResponse {

    public CkanUser result;
}

class TagListResponse extends CkanResponse {

    public List<CkanTag> result;
}

class OrganizationResponse extends CkanResponse {

    public CkanOrganization result;
}

class GroupResponse extends CkanResponse {

    public CkanGroup result;
}

class OrganizationListResponse extends CkanResponse {

    public List<CkanOrganization> result;
}

class GroupListResponse extends CkanResponse {

    public List<CkanGroup> result;
}

class GroupNamesResponse extends CkanResponse {

    public List<String> result;
}

class TagNamesResponse extends CkanResponse {

    public List<String> result;
}

class DatasetSearchResponse extends CkanResponse {

    public SearchResults<CkanDataset> result;
}

class LicenseListResponse extends CkanResponse {

    public List<CkanLicense> result;
}

class FormatListResponse extends CkanResponse {

    public Set<String> result;
}

class ApiVersionResponse {

    public int version;
}