2012 lines
72 KiB
Java
2012 lines
72 KiB
Java
/*
|
|
* 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 org.gcube.datacatalogue.utillibrary.jackan;
|
|
|
|
import static com.google.common.base.Preconditions.checkArgument;
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
import static org.gcube.datacatalogue.utillibrary.server.utils.GenericUtils.checkNotEmpty;
|
|
import static org.gcube.datacatalogue.utillibrary.server.utils.GenericUtils.removeTrailingSlash;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.net.URLEncoder;
|
|
import java.nio.charset.Charset;
|
|
import java.sql.Timestamp;
|
|
import java.text.DateFormat;
|
|
import java.text.ParseException;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.TimeZone;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
import org.apache.http.client.fluent.Request;
|
|
import org.apache.http.client.fluent.Response;
|
|
import org.apache.http.entity.ContentType;
|
|
import org.apache.http.entity.mime.MultipartEntityBuilder;
|
|
import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore;
|
|
import org.gcube.com.fasterxml.jackson.annotation.JsonInclude.Include;
|
|
import org.gcube.com.fasterxml.jackson.databind.DeserializationFeature;
|
|
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
|
|
import org.gcube.com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanDataset;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanDatasetBase;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanDatasetRelationship;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanError;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanGroup;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanGroupOrgBase;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanLicense;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanOrganization;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanPair;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanResource;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanResourceBase;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanResponse;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanTag;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanTagBase;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanUser;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanUserBase;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanVocabulary;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.CkanVocabularyBase;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.exceptions.CkanAuthorizationException;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.exceptions.CkanException;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.exceptions.CkanNotFoundException;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.exceptions.CkanValidationException;
|
|
import org.gcube.datacatalogue.utillibrary.shared.jackan.model.exceptions.JackanException;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import com.google.common.base.Charsets;
|
|
import com.google.common.base.Strings;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.io.CharStreams;
|
|
|
|
/**
|
|
* Client to access a ckan instance. Threadsafe.
|
|
* <p>
|
|
* The client is a thin wrapper upon Ckan api, thus one method call should
|
|
* correspond to only one web api call. This means sometimes to get a full
|
|
* object from Ckan, you will need to do a second call.
|
|
* </p>
|
|
* <p>
|
|
* You can create clients either with constructors or the
|
|
* {@link CkanClient#builder() builder()} method if you need to set more
|
|
* connection parameters (i.e. proxy, timeout, ..).
|
|
* </p>
|
|
* <p>
|
|
* For writing to Ckan you might want to use {@link CheckedCkanClient} which
|
|
* does additional checks to ensure written content is correct.
|
|
* </p>
|
|
*
|
|
* @author David Leoni, Ivan Tankoyeu
|
|
*
|
|
*/
|
|
public class CkanClient {
|
|
|
|
/**
|
|
* CKAN uses timestamps like '1970-01-01T01:00:00.000010' in UTC timezone,
|
|
* has precision up to microsecond and doesn't append 'Z' to timestamps. The
|
|
* format respects
|
|
* <a href="https://en.wikipedia.org/wiki/ISO_8601" target="_blank">ISO 8601
|
|
* standard</a>. In Jackan we store it as {@link java.sql.Timestamp} or
|
|
* {@code null} if parse is not successful.
|
|
*
|
|
* @see #parseTimestamp(java.lang.String)
|
|
* @see #formatTimestamp(java.sql.Timestamp)
|
|
*
|
|
* @since 0.4.1
|
|
*/
|
|
public static final String CKAN_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS";
|
|
|
|
/**
|
|
* Found pattern "2013-12-17T00:00:00" in resource.date_modified in
|
|
* dati.toscana:
|
|
* http://dati.toscana.it/api/3/action/package_show?id=alluvioni_bacreg See
|
|
* also <a href="https://github.com/ckan/ckan/issues/1874"> ckan issue
|
|
* 874 </a> and <a href="https://github.com/ckan/ckan/pull/2519">ckan pull
|
|
* 2519</a>
|
|
*
|
|
* @since 0.4.1
|
|
*/
|
|
public static final String CKAN_NO_MILLISECS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss";
|
|
|
|
/**
|
|
* Notice that even for the same api version (at least for versions up to 3
|
|
* included) different CKAN instances can behave quite differently, either
|
|
* for differences in software or custom server permissions.
|
|
*/
|
|
public static final ImmutableList<Integer> SUPPORTED_API_VERSIONS = ImmutableList.of(3);
|
|
|
|
/** Default timeout in millisecs */
|
|
public static final int DEFAULT_TIMEOUT = 15000;
|
|
|
|
/**
|
|
* Sometimes we get back Python "None" as a string instead of proper JSON
|
|
* null
|
|
*
|
|
* @since 0.4.1
|
|
*/
|
|
public static final String NONE = "None";
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(CkanClient.class.getName());
|
|
|
|
private static final String COULDNT_JSONIZE = "Couldn't jsonize the provided ";
|
|
|
|
@Nullable
|
|
private static ObjectMapper objectMapper;
|
|
|
|
private static final Map<String, ObjectMapper> OBJECT_MAPPERS_FOR_POSTING = new HashMap();
|
|
|
|
private String catalogUrl;
|
|
|
|
@Nullable
|
|
private String ckanToken;
|
|
|
|
@Nullable
|
|
private String proxy;
|
|
|
|
/** connection timeout in millisecs */
|
|
private int timeout;
|
|
|
|
@JsonSerialize(as = CkanResourceBase.class)
|
|
private abstract static class CkanResourceForPosting {
|
|
}
|
|
|
|
@JsonSerialize(as = CkanDatasetBase.class)
|
|
private abstract static class CkanDatasetForPosting {
|
|
}
|
|
|
|
@JsonSerialize(as = CkanGroupOrgBase.class)
|
|
private abstract static class CkanGroupOrgForPosting {
|
|
}
|
|
|
|
@JsonSerialize(as = GroupForDatasetPosting.class)
|
|
abstract static class GroupForDatasetPosting extends CkanGroupOrgBase {
|
|
|
|
@JsonIgnore
|
|
@Override
|
|
public List<CkanUser> getUsers() {
|
|
return super.getUsers();
|
|
}
|
|
}
|
|
|
|
@JsonSerialize(as = CkanUserBase.class)
|
|
private abstract static class CkanUserForPosting {
|
|
}
|
|
|
|
@JsonSerialize(as = CkanTagBase.class)
|
|
private abstract static class CkanTagForPosting {
|
|
}
|
|
|
|
/**
|
|
* Configures the provided Jackson ObjectMapper exactly as the internal JSON
|
|
* mapper used for reading operations. If you want to perform
|
|
* create/update/delete operations, use
|
|
* {@link #configureObjectMapperForPosting(com.fasterxml.jackson.databind.ObjectMapper, java.lang.Class) }
|
|
* instead.
|
|
*
|
|
* @param om
|
|
* a Jackson object mapper
|
|
* @since 0.4.1
|
|
*/
|
|
public static void configureObjectMapper(ObjectMapper om) {
|
|
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
|
|
|
om.registerModule(new JackanModule());
|
|
}
|
|
|
|
/**
|
|
* Configures the provided Jackson ObjectMapper for create/update/delete
|
|
* operations of Ckan objects. For reading and generic
|
|
* serialization/deserialization of Ckan objects, use
|
|
* {@link #configureObjectMapper(com.fasterxml.jackson.databind.ObjectMapper) }
|
|
* instead. For future compatibility you will need a different object mapper
|
|
* for each class you want to post to ckan. <b> DO NOT </b> call
|
|
* {@link #configureObjectMapper(com.fasterxml.jackson.databind.ObjectMapper) }
|
|
* on the mapper prior to this call.
|
|
*
|
|
* @param om
|
|
* a Jackson object mapper
|
|
* @param clazz
|
|
* the class of the objects you wish to create/update/delete.
|
|
* @since 0.4.1
|
|
*/
|
|
public static void configureObjectMapperForPosting(ObjectMapper om, Class clazz) {
|
|
configureObjectMapper(om);
|
|
om.setSerializationInclusion(Include.NON_NULL);
|
|
om.addMixInAnnotations(CkanResource.class, CkanResourceForPosting.class);
|
|
om.addMixInAnnotations(CkanDataset.class, CkanDatasetForPosting.class);
|
|
om.addMixInAnnotations(CkanOrganization.class, CkanGroupOrgForPosting.class);
|
|
if (CkanDatasetBase.class.isAssignableFrom(clazz)) {
|
|
// little fix for
|
|
// https://github.com/opendatatrentino/jackan/issues/19
|
|
om.addMixInAnnotations(CkanGroup.class, GroupForDatasetPosting.class);
|
|
} else {
|
|
om.addMixInAnnotations(CkanGroup.class, CkanGroupOrgForPosting.class);
|
|
}
|
|
|
|
om.addMixInAnnotations(CkanUser.class, CkanUserForPosting.class);
|
|
om.addMixInAnnotations(CkanTag.class, CkanTagForPosting.class);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Jackson object mapper configured for creation/update
|
|
* operations. Internally, Object mapper is initialized at first call.
|
|
*
|
|
* @param clazz
|
|
* the class you want to post. For generic class, just put
|
|
* Object.class
|
|
* @since 0.4.1
|
|
*/
|
|
static ObjectMapper getObjectMapperForPosting(Class clazz) {
|
|
checkNotNull(clazz, "Invalid class! If you don't know the class just use Object.class");
|
|
|
|
if (OBJECT_MAPPERS_FOR_POSTING.get(clazz.getName()) == null) {
|
|
LOG.info("Generating ObjectMapper for posting class {0}", clazz);
|
|
ObjectMapper om = new ObjectMapper();
|
|
configureObjectMapperForPosting(om, clazz);
|
|
OBJECT_MAPPERS_FOR_POSTING.put(clazz.getName(), om);
|
|
}
|
|
|
|
return OBJECT_MAPPERS_FOR_POSTING.get(clazz.getName());
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Jackson object mapper for reading operations. Internally,
|
|
* Object mapper is initialized at first call.
|
|
*/
|
|
static ObjectMapper getObjectMapper() {
|
|
if (objectMapper == null) {
|
|
objectMapper = new ObjectMapper();
|
|
configureObjectMapper(objectMapper);
|
|
}
|
|
return objectMapper;
|
|
}
|
|
|
|
/**
|
|
* The timeout expressed in milliseconds. By default it is
|
|
* {@link #DEFAULT_TIMEOUT}.
|
|
*/
|
|
public int getTimeout() {
|
|
return timeout;
|
|
}
|
|
|
|
protected CkanClient() {
|
|
this.timeout = DEFAULT_TIMEOUT;
|
|
this.catalogUrl = "";
|
|
}
|
|
|
|
/**
|
|
* Creates a Ckan client with null token
|
|
*
|
|
* @param catalogUrl
|
|
* the catalog url i.e. http://data.gov.uk. Internally, it will
|
|
* be stored in a normalized format (to avoid i.e. trailing
|
|
* slashes).
|
|
*/
|
|
public CkanClient(String catalogUrl) {
|
|
this();
|
|
checkNotEmpty(catalogUrl, "invalid ckan catalog url");
|
|
this.catalogUrl = removeTrailingSlash(catalogUrl);
|
|
}
|
|
|
|
/**
|
|
* Creates a Ckan client with null token
|
|
*
|
|
* @param catalogUrl
|
|
* 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 ckanToken
|
|
* the token used for authorization in ckan api
|
|
*
|
|
*/
|
|
public CkanClient(String catalogUrl, @Nullable String ckanToken) {
|
|
this(catalogUrl);
|
|
this.ckanToken = ckanToken;
|
|
}
|
|
|
|
/**
|
|
* Returns a new client builder.
|
|
*
|
|
* The builder is not threadsafe and you can use one builder instance to
|
|
* build only one client instance.
|
|
*
|
|
*/
|
|
public static CkanClient.Builder builder() {
|
|
return new Builder(new CkanClient());
|
|
}
|
|
|
|
/**
|
|
* Builder for the client. The builder is not threadsafe and you can use one
|
|
* builder instance to build only one client instance.
|
|
*
|
|
* @author David Leoni
|
|
*
|
|
*/
|
|
public static class Builder {
|
|
private CkanClient client;
|
|
private boolean created;
|
|
|
|
protected CkanClient getClient() {
|
|
return client;
|
|
}
|
|
|
|
protected boolean getCreated() {
|
|
return created;
|
|
}
|
|
|
|
protected void checkNotCreated() {
|
|
if (created) {
|
|
throw new IllegalStateException("Builder was already used to create a client!");
|
|
}
|
|
}
|
|
|
|
protected Builder(CkanClient client) {
|
|
checkNotNull(client);
|
|
this.client = client;
|
|
this.created = false;
|
|
}
|
|
|
|
/**
|
|
* Sets the catalog url i.e. http://data.gov.uk.
|
|
*
|
|
* Internally, it will be stored in a normalized format (to avoid i.e.
|
|
* trailing slashes).
|
|
*/
|
|
public Builder setCatalogUrl(String catalogUrl) {
|
|
checkNotCreated();
|
|
checkNotEmpty(catalogUrl, "invalid ckan catalog url");
|
|
this.client.catalogUrl = removeTrailingSlash(catalogUrl);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the private token string for ckan repository
|
|
*/
|
|
public Builder setCkanToken(@Nullable String ckanToken) {
|
|
checkNotCreated();
|
|
this.client.ckanToken = ckanToken;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the proxy used to perform GET and POST calls
|
|
*
|
|
* @param proxyUri the proxy used by the client, usually in a format with
|
|
* address and port like {@code my.own-proxy.org:1234}
|
|
*/
|
|
public Builder setProxy(@Nullable String proxyUri) {
|
|
checkNotCreated();
|
|
|
|
if (proxyUri == null) {
|
|
this.client.proxy = null;
|
|
} else {
|
|
this.client.proxy = removeTrailingSlash(proxyUri);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the connection timeout expressed as number of milliseconds. Must
|
|
* be greater than zero, otherwise IllegalArgumentException is thrown.
|
|
*
|
|
* @throws IllegalArgumentException
|
|
* is value is less than 1.
|
|
*/
|
|
public Builder setTimeout(int timeout) {
|
|
checkNotCreated();
|
|
checkArgument(timeout > 0, "Timeout must be > 0 ! Found instead %s", timeout);
|
|
this.client.timeout = timeout;
|
|
return this;
|
|
}
|
|
|
|
public CkanClient build() {
|
|
checkNotCreated();
|
|
checkNotEmpty(this.client.catalogUrl, "Invalid catalog url!");
|
|
this.created = true;
|
|
return this.client;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
String maskedToken = ckanToken == null ? null : "*****MASKED_TOKEN*******";
|
|
return "CkanClient{" + "catalogURL=" + catalogUrl + ", ckanToken=" + maskedToken + '}';
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configures the request. Should work both for GETs and POSTs.
|
|
*/
|
|
protected Request configureRequest(Request request) {
|
|
if (ckanToken != null) {
|
|
request.addHeader("Authorization", ckanToken);
|
|
}
|
|
if (proxy != null) {
|
|
request.viaProxy(proxy);
|
|
}
|
|
request.socketTimeout(this.timeout)
|
|
.connectTimeout(this.timeout);
|
|
|
|
return request;
|
|
}
|
|
|
|
/**
|
|
* Performs HTTP GET on server. If {@link CkanResponse#isSuccess()} is false
|
|
* throws {@link CkanException}.
|
|
*
|
|
* @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 CkanException
|
|
* on error
|
|
*/
|
|
private <T extends CkanResponse> T getHttp(Class<T> responseType, String path, Object... params) {
|
|
|
|
checkNotNull(responseType);
|
|
checkNotNull(path);
|
|
|
|
String fullUrl = calcFullUrl(path, params);
|
|
|
|
T ckanResponse;
|
|
String returnedText;
|
|
|
|
try {
|
|
LOG.info("getting {0}", fullUrl);
|
|
Request request = Request.Get(fullUrl);
|
|
|
|
configureRequest(request);
|
|
|
|
Response response = request.execute();
|
|
|
|
InputStream stream = response.returnResponse()
|
|
.getEntity()
|
|
.getContent();
|
|
|
|
try (InputStreamReader reader = new InputStreamReader(stream, Charsets.UTF_8)) {
|
|
returnedText = CharStreams.toString(reader);
|
|
}
|
|
} catch (Exception ex) {
|
|
throw new CkanException("Error while performing GET. Request url was: " + fullUrl, this, ex);
|
|
}
|
|
try {
|
|
ckanResponse = getObjectMapper().readValue(returnedText, responseType);
|
|
} catch (Exception ex) {
|
|
throw new CkanException(
|
|
"Couldn't interpret json returned by the server! Returned text was: " + returnedText, this, ex);
|
|
}
|
|
|
|
if (!ckanResponse.isSuccess()) {
|
|
throwCkanException("Error while performing GET. Request url was: " + fullUrl, ckanResponse);
|
|
}
|
|
return ckanResponse;
|
|
}
|
|
|
|
/**
|
|
* Throws {@link CkanException} or a subclass of it according to
|
|
* {@link CkanError#getType()}
|
|
*
|
|
* @throws CkanException
|
|
* @since 0.4.1
|
|
*/
|
|
protected <T extends CkanResponse> void throwCkanException(String msg, T ckanResponse) {
|
|
if (ckanResponse.getError() != null && ckanResponse.getError()
|
|
.getType() != null) {
|
|
switch (ckanResponse.getError()
|
|
.getType()) {
|
|
case CkanError.NOT_FOUND_ERROR:
|
|
throw new CkanNotFoundException(msg, ckanResponse, this);
|
|
case CkanError.VALIDATION_ERROR:
|
|
throw new CkanValidationException(msg, ckanResponse, this);
|
|
case CkanError.AUTHORIZATION_ERROR:
|
|
throw new CkanAuthorizationException(msg, ckanResponse, this);
|
|
}
|
|
|
|
}
|
|
|
|
throw new CkanException(msg, ckanResponse, this);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* POSTs a body via HTTP. If {@link CkanResponse#isSuccess()} is false
|
|
* throws {@link CkanException}.
|
|
*
|
|
* @param responseType
|
|
* a descendant of CkanResponse
|
|
* @param path
|
|
* something like /api/3/action/package_create
|
|
* @param body
|
|
* the body of the POST
|
|
* @param contentType
|
|
* @param params
|
|
* list of key, value parameters. They must be not be url
|
|
* encoded. i.e. "id","laghi-monitorati-trento"
|
|
* @throws CkanException
|
|
* on error
|
|
*/
|
|
private <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);
|
|
|
|
T ckanResponse;
|
|
String returnedText;
|
|
|
|
try {
|
|
LOG.info("Posting to url {0}", fullUrl);
|
|
LOG.info("Sending body:{0}", body);
|
|
Request request = Request.Post(fullUrl);
|
|
|
|
configureRequest(request);
|
|
|
|
Response response = request.bodyString(body, contentType)
|
|
.execute();
|
|
|
|
InputStream stream = response.returnResponse()
|
|
.getEntity()
|
|
.getContent();
|
|
|
|
try (InputStreamReader reader = new InputStreamReader(stream, Charsets.UTF_8)) {
|
|
returnedText = CharStreams.toString(reader);
|
|
}
|
|
} catch (Exception ex) {
|
|
throw new CkanException("Error while performing a POST! Request url is:" + fullUrl, this, ex);
|
|
}
|
|
|
|
try {
|
|
ckanResponse = getObjectMapper().readValue(returnedText, responseType);
|
|
} catch (Exception ex) {
|
|
throw new CkanException(
|
|
"Couldn't interpret json returned by the server! Returned text was: " + returnedText, this, ex);
|
|
}
|
|
|
|
if (!ckanResponse.isSuccess()) {
|
|
throwCkanException("Error while performing a POST! Request url is:" + fullUrl, ckanResponse);
|
|
}
|
|
return ckanResponse;
|
|
|
|
}
|
|
|
|
/**
|
|
* Update the data associated to the given resource.
|
|
*
|
|
* @param responseType
|
|
* a descendant of CkanResponse
|
|
* @param path
|
|
* something like /api/3/action/package_create
|
|
* @param resource
|
|
* a {@link CkanResourceBase} object with a data file set in the
|
|
* {@link CkanResourceBase#upload} field
|
|
*
|
|
* @throws CkanException
|
|
*/
|
|
private <T extends CkanResponse> T postHttpResourceFile(Class<T> responseType, String path,
|
|
CkanResourceBase resource) {
|
|
checkNotNull(responseType);
|
|
checkNotNull(path);
|
|
checkNotNull(resource);
|
|
checkNotNull(resource.getUpload());
|
|
checkNotNull(resource.getPackageId());
|
|
|
|
String fullUrl = calcFullUrl(path, new Object[] {});
|
|
|
|
T ckanResponse;
|
|
String returnedText;
|
|
|
|
try {
|
|
LOG.info("Posting to url {0}", fullUrl);
|
|
Request request = Request.Post(fullUrl);
|
|
|
|
configureRequest(request);
|
|
|
|
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
|
|
.addBinaryBody("upload", resource.getUpload(),
|
|
ContentType.create(
|
|
"application/octet-stream",
|
|
Charset.forName("UTF-8")),
|
|
resource.getUpload()
|
|
.getName())
|
|
.addTextBody("size", resource.getSize(),
|
|
ContentType.TEXT_PLAIN)
|
|
.addTextBody("id", resource.getId(),
|
|
ContentType.TEXT_PLAIN)
|
|
.addTextBody("url", "upload",
|
|
ContentType.TEXT_PLAIN)
|
|
.addTextBody("package_id",
|
|
resource.getPackageId(),
|
|
ContentType.TEXT_PLAIN);
|
|
|
|
if (resource.getFormat() != null)
|
|
entityBuilder.addTextBody("format", resource.getFormat(), ContentType.TEXT_PLAIN);
|
|
if (resource.getMimetype() != null)
|
|
entityBuilder.addTextBody("mimetype", resource.getMimetype(), ContentType.TEXT_PLAIN);
|
|
if (resource.getLastModified() != null)
|
|
entityBuilder.addTextBody("last_modified", resource.getLastModified(), ContentType.TEXT_PLAIN);
|
|
|
|
entityBuilder.setCharset(Charset.forName("UTF-8"));
|
|
|
|
Response response = request.body(entityBuilder.build())
|
|
.execute();
|
|
|
|
InputStream stream = response.returnResponse()
|
|
.getEntity()
|
|
.getContent();
|
|
|
|
try (InputStreamReader reader = new InputStreamReader(stream, Charsets.UTF_8)) {
|
|
returnedText = CharStreams.toString(reader);
|
|
}
|
|
} catch (Exception ex) {
|
|
throw new CkanException("Error while performing a POST! Request url is:" + fullUrl, this, ex);
|
|
}
|
|
|
|
try {
|
|
ckanResponse = getObjectMapper().readValue(returnedText, responseType);
|
|
} catch (Exception ex) {
|
|
throw new CkanException(
|
|
"Couldn't interpret json returned by the server! Returned text was: " + returnedText, this, ex);
|
|
}
|
|
|
|
if (!ckanResponse.isSuccess()) {
|
|
throwCkanException("Error while performing a POST! Request url is:" + fullUrl, ckanResponse);
|
|
}
|
|
return ckanResponse;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns the catalog URL (normalized).
|
|
*/
|
|
public String getCatalogUrl() {
|
|
return catalogUrl;
|
|
}
|
|
|
|
/**
|
|
* Returns the private CKAN token.
|
|
*/
|
|
public String getCkanToken() {
|
|
return ckanToken;
|
|
}
|
|
|
|
private static void checkCatalogUrl(String catalogUrl) {
|
|
checkNotEmpty(catalogUrl, "invalid catalog url");
|
|
}
|
|
|
|
/**
|
|
* Returns the URL of dataset page in the catalog website.
|
|
*
|
|
* Valid URLs have this format with the name: <a href=
|
|
* "http://dati.trentino.it/dataset/impianti-di-risalita-vivifiemme-2013"
|
|
* target="_blank"> http://dati.trentino.it/dataset/impianti-di-risalita-
|
|
* vivifiemme-2013 </a>
|
|
*
|
|
* @param datasetIdOrName
|
|
* either the dataset's {@link CkanDataset#getId() alphanumerical
|
|
* id} (preferred as it is more stable) or the
|
|
* {@link CkanDataset#getName() dataset name}
|
|
*
|
|
* @param catalogUrl
|
|
* i.e. http://dati.trentino.it
|
|
*/
|
|
public static String makeDatasetUrl(String catalogUrl, String datasetIdOrName) {
|
|
checkCatalogUrl(catalogUrl);
|
|
checkNotEmpty(datasetIdOrName, "invalid dataset identifier");
|
|
return removeTrailingSlash(catalogUrl) + "/dataset/" + datasetIdOrName;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Returns the URL of resource page in the catalog website.
|
|
*
|
|
* Valid URLs have this format with the dataset name
|
|
* 'impianti-di-risalita-vivifiemme-2013': <a href=
|
|
* "http://dati.trentino.it/dataset/impianti-di-risalita-vivifiemme-2013/resource/779d1d9d-9370-47f4-a194-1b0328c32f02"
|
|
* target="_blank"> http://dati.trentino.it/dataset/impianti-di-risalita-
|
|
* vivifiemme-2013/resource/779d1d9d-9370-47f4-a194-1b0328c32f02</a>
|
|
*
|
|
* @param catalogUrl
|
|
* i.e. http://dati.trentino.it
|
|
* @param datasetIdOrName
|
|
* either the dataset's alphanumerical {@link CkanDataset#getId()
|
|
* id} (preferred as it is more stable) or the
|
|
* {@link CkanDataset#getName() dataset name}
|
|
*
|
|
* @param resourceId
|
|
* the {@link CkanResource#getId() alphanumerical id} of the
|
|
* resource (DON'T use {@link CkanResource#getName() resource
|
|
* name})
|
|
*/
|
|
public static String makeResourceUrl(String catalogUrl, String datasetIdOrName, String resourceId) {
|
|
checkCatalogUrl(catalogUrl);
|
|
checkNotEmpty(datasetIdOrName, "invalid dataset identifier");
|
|
checkNotEmpty(resourceId, "invalid resource id");
|
|
return makeDatasetUrl(catalogUrl, datasetIdOrName) + "/resource/" + resourceId;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Reconstructs the URL of group page in the catalog website.
|
|
*
|
|
* Valid URLs have this format with the group name
|
|
* 'gestione-del-territorio':
|
|
*
|
|
* <a href="http://dati.trentino.it/group/gestione-del-territorio" target=
|
|
* "_blank"> http://dati.trentino.it/group/gestione-del-territorio </a>
|
|
*
|
|
* @param catalogUrl
|
|
* i.e. http://dati.trentino.it
|
|
* @param groupIdOrName
|
|
* the group's alphanumerical id (preferred as more stable) or
|
|
* the group name as in {@link CkanGroup#getName()}.
|
|
*/
|
|
public static String makeGroupUrl(String catalogUrl, String groupIdOrName) {
|
|
checkCatalogUrl(catalogUrl);
|
|
checkNotEmpty(groupIdOrName, "invalid group identifier");
|
|
return removeTrailingSlash(catalogUrl) + "/group/" + groupIdOrName;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Given some organization parameters, reconstruct the URL of organization
|
|
* page in the catalog website.
|
|
*
|
|
* Valid URLs have this format with the organization name
|
|
* 'comune-di-trento':
|
|
*
|
|
* <a href="http://dati.trentino.it/organization/comune-di-trento" target=
|
|
* "_blank"> http://dati.trentino.it/organization/comune-di-trento </a>
|
|
*
|
|
* @param catalogUrl
|
|
* i.e. http://dati.trentino.it
|
|
* @param orgIdOrName
|
|
* the organization's alphanumerical id (preferred as more
|
|
* stable),
|
|
* or the name as in {@link CkanOrganization#getName()}
|
|
*/
|
|
public static String makeOrganizationUrl(String catalogUrl, String orgIdOrName) {
|
|
checkCatalogUrl(catalogUrl);
|
|
checkNotEmpty(orgIdOrName, "invalid organization identifier");
|
|
return removeTrailingSlash(catalogUrl) + "/organization/" + orgIdOrName;
|
|
}
|
|
|
|
/**
|
|
* Returns list of dataset names 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(int limit, int 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 = 5; i >= 1; i--) { // this is demential. But /api always
|
|
// gives { "version": 1} ....
|
|
try {
|
|
return getApiVersion(i);
|
|
} catch (Exception ex) {
|
|
|
|
}
|
|
}
|
|
throw new CkanException("Error while getting api version!", this);
|
|
}
|
|
|
|
/**
|
|
* Returns the given api number
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
*/
|
|
private synchronized int getApiVersion(int number) {
|
|
String fullUrl = catalogUrl + "/api/" + number;
|
|
LOG.info("getting {0}", fullUrl);
|
|
try {
|
|
Request request = Request.Get(fullUrl);
|
|
configureRequest(request);
|
|
String json = request.execute()
|
|
.returnContent()
|
|
.asString();
|
|
|
|
return getObjectMapper().readValue(json, ApiVersionResponse.class).version;
|
|
} catch (Exception ex) {
|
|
throw new CkanException("Error while fetching api version!", this, ex);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Fetches the dataset from ckan. Returned dataset will have resources with
|
|
* at least all of the fields of {@link CkanResourceBase}
|
|
*
|
|
* @param idOrName
|
|
* either the dataset name (i.e. certified-products) or the
|
|
* alphanumerical id (i.e. 22eea137-9fc3-4222-a716-bac22cc2039a)
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
* @throws CkanNotFoundException
|
|
* if dataset is missing
|
|
*
|
|
*/
|
|
public synchronized CkanDataset getDataset(String idOrName) {
|
|
checkNotNull(idOrName, "Need a valid id or name!");
|
|
|
|
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 CkanException
|
|
* on error
|
|
*/
|
|
public synchronized List<CkanUser> getUserList() {
|
|
return getHttp(UserListResponse.class, "/api/3/action/user_list").result;
|
|
}
|
|
|
|
/**
|
|
* @param id
|
|
* i.e. 'admin'
|
|
* @throws CkanException
|
|
* on error
|
|
* @throws CkanNotFoundException
|
|
* if user is missing
|
|
*/
|
|
public synchronized CkanUser getUser(String id) {
|
|
checkNotNull(id, "Need a valid id!");
|
|
|
|
return getHttp(UserResponse.class, "/api/3/action/user_show", "id", id).result;
|
|
}
|
|
|
|
/**
|
|
* Creates ckan user on the server.
|
|
*
|
|
* @param user
|
|
* ckan user object with the minimal set of parameters required.
|
|
* See
|
|
* {@link CkanUserBase#CkanUserBase(java.lang.String, java.lang.String, java.lang.String)
|
|
* this constructor}
|
|
* @return the newly created user
|
|
* @throws JackanException
|
|
*/
|
|
public synchronized CkanUser createUser(CkanUserBase user) {
|
|
checkNotNull(user, "Need a valid user!");
|
|
|
|
checkToken("Tried to create user" + user.getName());
|
|
|
|
ObjectMapper om = CkanClient.getObjectMapperForPosting(CkanUserBase.class);
|
|
String json = null;
|
|
|
|
try {
|
|
json = om.writeValueAsString(user);
|
|
} catch (IOException e) {
|
|
throw new CkanException(COULDNT_JSONIZE + user.getClass()
|
|
.getSimpleName(),
|
|
this, e);
|
|
|
|
}
|
|
|
|
return postHttp(UserResponse.class, "/api/3/action/user_create", json, ContentType.APPLICATION_JSON).result;
|
|
}
|
|
|
|
/**
|
|
* @param id
|
|
* The alphanumerical id of the resource, such as
|
|
* d0892ada-b8b9-43b6-81b9-47a86be126db.
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
* @throws CkanNotFoundException
|
|
* if resource is missing
|
|
*/
|
|
public synchronized CkanResource getResource(String id) {
|
|
checkNotNull(id, "Need a valid id!");
|
|
return getHttp(ResourceResponse.class, "/api/3/action/resource_show", "id", id).result;
|
|
}
|
|
|
|
/**
|
|
* Creates ckan resource on the server.
|
|
*
|
|
* @param resource
|
|
* ckan resource object with the minimal set of parameters
|
|
* required. See
|
|
* {@link CkanResource#CkanResource(String, String)}
|
|
* @return the newly created resource
|
|
* @throws JackanException
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanResource createResource(CkanResourceBase resource) {
|
|
checkNotNull(resource, "Need a valid resource!");
|
|
|
|
checkToken("Tried to create resource " + resource.getName());
|
|
|
|
ObjectMapper om = CkanClient.getObjectMapperForPosting(CkanResourceBase.class);
|
|
String json = null;
|
|
try {
|
|
json = om.writeValueAsString(resource);
|
|
} catch (IOException e) {
|
|
throw new CkanException(COULDNT_JSONIZE + resource.getClass()
|
|
.getSimpleName(),
|
|
this, e);
|
|
|
|
}
|
|
|
|
if (resource.getUpload() == null) {
|
|
return postHttp(ResourceResponse.class, "/api/3/action/resource_create", json,
|
|
ContentType.APPLICATION_JSON).result;
|
|
} else {
|
|
// Could not find a way to create a resource with an attached file
|
|
// without enumerating all the resource
|
|
// fields so doing it in two steps :
|
|
// First, create the resource as usual
|
|
// Then, update it with the file
|
|
CkanResource resourceResponse = postHttp(ResourceResponse.class, "/api/3/action/resource_create", json,
|
|
ContentType.APPLICATION_JSON).result;
|
|
resource.setId(resourceResponse.getId());
|
|
return postHttpResourceFile(ResourceResponse.class, "/api/3/action/resource_update", resource).result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a resource on the server using a straight {@code resource_update}
|
|
* call. Null fields will not be sent and thus won't get updated, but be
|
|
* careful about custom fields of {@link CkanResourceBase#getOthers()}, if
|
|
* not sent they will be erased on the server! To prevent this behaviour,
|
|
* see {@link #patchUpdateResource(CkanResourceBase) }
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanResource updateResource(CkanResourceBase resource) {
|
|
checkNotNull(resource, "Need a valid resource!");
|
|
checkToken("Tried to update resource" + resource.getName());
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanResourceBase.class).writeValueAsString(resource);
|
|
} catch (IOException ex) {
|
|
throw new CkanException(COULDNT_JSONIZE + resource.getClass()
|
|
.getSimpleName(),
|
|
this, ex);
|
|
|
|
}
|
|
|
|
return postHttp(ResourceResponse.class, "/api/3/action/resource_update", json,
|
|
ContentType.APPLICATION_JSON).result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Jackan specific. Patches a resource on the ckan server using a
|
|
* {@code resource_update} call. Todo: this is a temporary solution until we
|
|
* implement new {@code patch} api of CKAN 2.3
|
|
*
|
|
* @param resource
|
|
* ckan resource object. Fields set to {@code null} won't be
|
|
* updated on the server. Items present in lists such as
|
|
* {@link CkanResourceBase#getOthers() others} will be added to
|
|
* existing ones on the server. To support this behaviour
|
|
* provided {@code resource} might be patched with latest
|
|
* metadata from the server prior sending it for update.
|
|
*
|
|
* @see #updateResource(CkanResourceBase)
|
|
* @throws CkanException
|
|
* on error
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanResource patchUpdateResource(CkanResourceBase resource) {
|
|
checkNotNull(resource, "Need a valid resource!");
|
|
checkToken("Tried to update resource" + resource.getName());
|
|
|
|
CkanResource origResource = getResource(resource.getId());
|
|
// others
|
|
Map<String, Object> newOthers = new HashMap();
|
|
|
|
if (origResource.getOthers() != null) {
|
|
newOthers.putAll(origResource.getOthers());
|
|
}
|
|
if (resource.getOthers() != null) {
|
|
newOthers.putAll(resource.getOthers());
|
|
}
|
|
resource.setOthers(newOthers);
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanResourceBase.class).writeValueAsString(resource);
|
|
} catch (IOException ex) {
|
|
throw new CkanException(COULDNT_JSONIZE + resource.getClass()
|
|
.getSimpleName(),
|
|
this, ex);
|
|
|
|
}
|
|
|
|
return postHttp(ResourceResponse.class, "/api/3/action/resource_update", json,
|
|
ContentType.APPLICATION_JSON).result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Update the data file associated to this resource.
|
|
*
|
|
* @param resource
|
|
* a {@link CkanResourceBase} object with a data file set in the
|
|
* {@link CkanResourceBase#upload} field
|
|
* @throws CkanException
|
|
*
|
|
* @since 0.4.3
|
|
*
|
|
*/
|
|
public synchronized CkanResource updateResourceData(CkanResourceBase resource) {
|
|
checkNotNull(resource);
|
|
checkNotNull(resource.getUpload(), "Need a non null file in resource.getUpload() !");
|
|
checkToken("Tried to update resource" + resource.getName());
|
|
|
|
return postHttpResourceFile(ResourceResponse.class, "/api/3/action/resource_update", resource).result;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* Marks a resource as {@code 'deleted'}.
|
|
*
|
|
* Note this will just set resource state to
|
|
* {@link eu.trentorise.opendata.jackan.model.CkanState#deleted} and make it
|
|
* inaccessible from the website and api.
|
|
*
|
|
* @param id
|
|
* The alphanumerical id of the resource, such as
|
|
* d0892ada-b8b9-43b6-81b9-47a86be126db.
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
* @throws CkanNotFoundException
|
|
* if trying to delete non-existing resource
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized void deleteResource(String id) {
|
|
checkNotNull(id, "Need a valid id!");
|
|
checkToken("Tried to delete resource with id " + id);
|
|
|
|
String json = "{\"id\":\"" + id + "\"}";
|
|
postHttp(ResourceResponse.class, "/api/3/action/resource_delete", json, ContentType.APPLICATION_JSON);
|
|
}
|
|
|
|
/**
|
|
* 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 CkanException
|
|
* on error
|
|
*/
|
|
public synchronized List<CkanGroup> getGroupList() {
|
|
return getHttp(GroupListResponse.class, "/api/3/action/group_list", "all_fields", "True").result;
|
|
}
|
|
|
|
/**
|
|
* Return group names, like i.e. management-of-territory
|
|
*
|
|
* @throws CkanException
|
|
* 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 (i.e.
|
|
* 55bb5fbd-7a7c-4eb8-8b1a-1192a5504421)
|
|
* @throws CkanException
|
|
* on error
|
|
* @throws CkanNotFoundException
|
|
* if group is missing
|
|
*/
|
|
public synchronized CkanGroup getGroup(String idOrName) {
|
|
checkNotNull(idOrName, "Need a valid id or name!");
|
|
return getHttp(GroupResponse.class, "/api/3/action/group_show", "id", idOrName, "include_datasets",
|
|
"false").result;
|
|
}
|
|
|
|
/**
|
|
* Returns the organizations present in CKAN.
|
|
*
|
|
* @see #getGroupList()
|
|
*
|
|
* @throws CkanException
|
|
* 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 CkanException
|
|
* on error
|
|
*/
|
|
public synchronized Set<String> getFormats() {
|
|
return getHttp(FormatListResponse.class, "/api/3/action/format_autocomplete", "q", "", "limit", "1000").result;
|
|
}
|
|
|
|
/**
|
|
* @throws CkanException
|
|
* on error
|
|
*/
|
|
public synchronized List<String> getOrganizationNames() {
|
|
return getHttp(GroupNamesResponse.class, "/api/3/action/organization_list").result;
|
|
}
|
|
|
|
/**
|
|
* Returns a Ckan organization.
|
|
*
|
|
* @param idOrName
|
|
* 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 CkanException
|
|
* on error
|
|
* @throws CkanNotFoundException
|
|
* if organization is missing
|
|
*/
|
|
public synchronized CkanOrganization getOrganization(String idOrName) {
|
|
checkNotNull(idOrName, "Need a valid id or name!");
|
|
return getHttp(OrganizationResponse.class, "/api/3/action/organization_show", "id", idOrName,
|
|
"include_datasets", "false").result;
|
|
}
|
|
|
|
/**
|
|
* Creates CkanTag on the server.
|
|
*
|
|
* @param tag
|
|
* Ckan tag without id
|
|
* @return the newly created tag
|
|
* @throws JackanException
|
|
*/
|
|
public synchronized CkanTag createTag(CkanTagBase tag) {
|
|
checkNotNull(tag, "Need a valid tag!");
|
|
|
|
checkToken("Tried to create tag" + tag.getName());
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanTagBase.class).writeValueAsString(tag);
|
|
} catch (IOException e) {
|
|
throw new CkanException(COULDNT_JSONIZE + tag.getClass()
|
|
.getSimpleName(),
|
|
this, e);
|
|
|
|
}
|
|
TagResponse response = postHttp(TagResponse.class, "/api/3/action/tag_create", json,
|
|
ContentType.APPLICATION_JSON);
|
|
return response.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 CkanException
|
|
* 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 CkanException
|
|
* on error
|
|
*/
|
|
public synchronized List<String> getTagNamesList(String query) {
|
|
checkNotNull(query, "Need a valid query!");
|
|
return getHttp(TagNamesResponse.class, "/api/3/action/tag_list", "query", query).result;
|
|
}
|
|
|
|
/**
|
|
* @throws CkanException
|
|
* on error
|
|
*/
|
|
public synchronized List<String> getTagNamesList() {
|
|
return getHttp(TagNamesResponse.class, "/api/3/action/tag_list").result;
|
|
}
|
|
|
|
/**
|
|
* Creates CkanVocabulary on the server.
|
|
*
|
|
* @param vocabulary
|
|
* Ckan vocabulary without id
|
|
* @return the newly created vocabulary
|
|
* @throws JackanException
|
|
*/
|
|
public synchronized CkanVocabulary createVocabulary(CkanVocabularyBase vocabulary) {
|
|
checkNotNull(vocabulary, "Need a valid vocabulary!");
|
|
|
|
checkToken("Tried to create vocabulary" + vocabulary.getName());
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanVocabularyBase.class).writeValueAsString(vocabulary);
|
|
} catch (IOException e) {
|
|
throw new CkanException(COULDNT_JSONIZE + vocabulary.getClass()
|
|
.getSimpleName(),
|
|
this, e);
|
|
|
|
}
|
|
VocabularyResponse response = postHttp(VocabularyResponse.class, "/api/3/action/vocabulary_create", json,
|
|
ContentType.APPLICATION_JSON);
|
|
return response.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) {
|
|
checkNotNull(fqPrefix, "Need a valid prefix!");
|
|
checkNotNull(key, "Need a valid key!");
|
|
checkNotNull(list, "Need a valid list!");
|
|
checkNotNull(fq, "Need a valid string builder!");
|
|
|
|
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 "";
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Parses a {@link #CKAN_TIMESTAMP_PATTERN Ckan timestamp} into a Java Timestamp.
|
|
* For resilience, it also accepts patterns without fractional part and with
|
|
* only millisecs.
|
|
*
|
|
* @throws IllegalArgumentException
|
|
* if timestamp can't be parsed.
|
|
* @see #formatTimestamp(java.sql.Timestamp) for the inverse process.
|
|
* @since 0.4.1
|
|
*/
|
|
// NOTE:
|
|
// - Timestamp.valueOf can't be used as it is locale timezone dependent...
|
|
// - new Timestamp(long) internally stores time without millisecs, and millisecs go to nano part
|
|
public static Timestamp parseTimestamp(String timestamp) {
|
|
if (timestamp == null) {
|
|
throw new IllegalArgumentException("Found null timestamp!");
|
|
}
|
|
|
|
if (NONE.equals(timestamp)) {
|
|
throw new IllegalArgumentException("Found timestamp with 'None' inside!");
|
|
}
|
|
|
|
String[] tokens = timestamp.split("\\.");
|
|
String withoutFractional;
|
|
|
|
int nanoSecs;
|
|
|
|
if (tokens.length == 2) {
|
|
withoutFractional = tokens[0];
|
|
|
|
int factor;
|
|
if (tokens[1].length() == 6){
|
|
factor = 1000;
|
|
} else if (tokens[1].length() == 3){
|
|
factor = 1000000;
|
|
} else {
|
|
throw new IllegalArgumentException("Couldn0t parse timestamp:" + timestamp
|
|
+ " ! unsupported fractional length: " + tokens[1].length());
|
|
}
|
|
try {
|
|
nanoSecs = Integer.parseInt(tokens[1]) * factor;
|
|
|
|
} catch (NumberFormatException ex){
|
|
throw new IllegalArgumentException("Couldn0t parse timestamp:" + timestamp
|
|
+ " ! invalid fractional part:" + tokens[1]);
|
|
}
|
|
|
|
} else if (tokens.length == 1){
|
|
withoutFractional = timestamp;
|
|
nanoSecs = 0;
|
|
} else {
|
|
throw new IllegalArgumentException("Error while parsing timestamp:" + timestamp);
|
|
}
|
|
|
|
try {
|
|
DateFormat formatter = new SimpleDateFormat(CKAN_NO_MILLISECS_PATTERN);
|
|
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
long time = formatter.parse(withoutFractional).getTime();
|
|
Timestamp ret = new Timestamp(time);
|
|
ret.setNanos(nanoSecs);
|
|
return ret;
|
|
|
|
} catch (ParseException ex) {
|
|
throw new IllegalArgumentException("Error while parsing timestamp:" + timestamp, ex);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Formats a timestamp according to {@link #CKAN_TIMESTAMP_PATTERN}, with
|
|
* precision up to microseconds.
|
|
*
|
|
* @throws IllegalArgumentException
|
|
* if timestamp can't be parsed.
|
|
* @see #parseTimestamp(java.lang.String) for the inverse process.
|
|
* @since 0.4.1
|
|
*/
|
|
// NOTE: Timestamp.toString can't be used as it is locale timezone dependent...
|
|
public static String formatTimestamp(Timestamp timestamp) {
|
|
|
|
if (timestamp == null) {
|
|
throw new IllegalArgumentException("Found null timestamp!");
|
|
}
|
|
DateFormat formatter = new SimpleDateFormat(CKAN_NO_MILLISECS_PATTERN);
|
|
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
String withoutFractional = formatter.format(timestamp);
|
|
|
|
int micros = timestamp.getNanos() / 1000;
|
|
String microsString = Strings.padStart(Integer.toString(micros), 6, '0');
|
|
|
|
return withoutFractional + "." + microsString;
|
|
|
|
}
|
|
|
|
/**
|
|
* @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 according to the provided query.
|
|
*
|
|
* @param query
|
|
* The query object
|
|
* @param limit
|
|
* maximum results to return
|
|
* @param offset
|
|
* search begins from offset
|
|
* @throws CkanException
|
|
* on error
|
|
*/
|
|
public synchronized SearchResults<CkanDataset> searchDatasets(CkanQuery query, int limit, int offset) {
|
|
checkNotNull(query, "Need a valid query!");
|
|
|
|
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());
|
|
|
|
for (CkanDataset ds : dsr.result.getResults()) {
|
|
for (CkanResource cr : ds.getResources()) {
|
|
cr.setPackageId(ds.getId());
|
|
}
|
|
}
|
|
|
|
return dsr.result;
|
|
}
|
|
|
|
/**
|
|
* @msg the prepended error message.
|
|
* @throws CkanException
|
|
*/
|
|
private void checkToken(@Nullable String prependedErrorMessage) {
|
|
if (ckanToken == null) {
|
|
throw new CkanException(String.valueOf(prependedErrorMessage) + ", but ckan token was not set!", this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates CkanDataset on the server. Will also create eventual resources
|
|
* present in the dataset.
|
|
*
|
|
* @param dataset
|
|
* Ckan dataset without id
|
|
* @return the newly created dataset
|
|
* @throws CkanException
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanDataset createDataset(CkanDatasetBase dataset) {
|
|
checkNotNull(dataset, "Need a valid dataset!");
|
|
|
|
checkToken("Tried to create dataset" + dataset.getName());
|
|
|
|
String json = null;
|
|
try {
|
|
|
|
json = getObjectMapperForPosting(CkanDatasetBase.class).writeValueAsString(dataset);
|
|
} catch (IOException e) {
|
|
throw new CkanException(COULDNT_JSONIZE + dataset.getClass()
|
|
.getSimpleName(),
|
|
this, e);
|
|
}
|
|
|
|
DatasetResponse response = postHttp(DatasetResponse.class, "/api/3/action/package_create", json,
|
|
ContentType.APPLICATION_JSON);
|
|
|
|
return response.result;
|
|
}
|
|
|
|
/**
|
|
* Updates a dataset on the ckan server using a straight
|
|
* {@code package_update} call. Null fields will not be sent and thus won't
|
|
* get updated, but be careful about list fields, if not sent they will be
|
|
* erased on the server! To prevent this behaviour, see
|
|
* {@link #patchUpdateDataset(CkanDatasetBase)}
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanDataset updateDataset(CkanDatasetBase dataset) {
|
|
checkNotNull(dataset, "Need a valid dataset!");
|
|
|
|
checkToken("Tried to update dataset" + dataset.getName());
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanDatasetBase.class).writeValueAsString(dataset);
|
|
} catch (IOException ex) {
|
|
throw new CkanException(COULDNT_JSONIZE + dataset.getClass()
|
|
.getSimpleName(),
|
|
this, ex);
|
|
|
|
}
|
|
|
|
return postHttp(DatasetResponse.class, "/api/3/action/package_update", json,
|
|
ContentType.APPLICATION_JSON).result;
|
|
|
|
}
|
|
|
|
public static List<CkanPair> extrasMapToList(Map<String, String> map) {
|
|
ArrayList ret = new ArrayList();
|
|
|
|
for (String key : map.keySet()) {
|
|
ret.add(new CkanPair(key, map.get(key)));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
private void mergeResources(@Nullable List<CkanResource> resourcesToMerge, List<CkanResource> targetResources) {
|
|
if (resourcesToMerge != null) {
|
|
for (CkanResource resourceToMerge : resourcesToMerge) {
|
|
boolean replaced = false;
|
|
for (int i = 0; i < targetResources.size(); i++) {
|
|
CkanResource targetRes = targetResources.get(i);
|
|
if (resourceToMerge.getId() != null && resourceToMerge.getId()
|
|
.equals(targetRes.getId())) {
|
|
targetResources.set(i, resourceToMerge);
|
|
replaced = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!replaced) {
|
|
targetResources.add(resourceToMerge);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void mergeGroups(@Nullable List<CkanGroup> groupsToMerge, List<CkanGroup> targetGroups) {
|
|
if (groupsToMerge != null) {
|
|
for (CkanGroup groupToMerge : groupsToMerge) {
|
|
boolean replaced = false;
|
|
for (int i = 0; i < targetGroups.size(); i++) {
|
|
CkanGroup targetRes = targetGroups.get(i);
|
|
if (groupToMerge.getId() != null && groupToMerge.getId()
|
|
.equals(targetRes.getId())) {
|
|
targetGroups.set(i, groupToMerge);
|
|
replaced = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!replaced) {
|
|
targetGroups.add(groupToMerge);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void mergeRelationships(@Nullable List<CkanDatasetRelationship> relationshipsToMerge,
|
|
List<CkanDatasetRelationship> targetDatasetRelationships) {
|
|
if (relationshipsToMerge != null) {
|
|
for (CkanDatasetRelationship relationshipToMerge : relationshipsToMerge) {
|
|
boolean replaced = false;
|
|
for (int i = 0; i < targetDatasetRelationships.size(); i++) {
|
|
CkanDatasetRelationship targetRes = targetDatasetRelationships.get(i);
|
|
if (relationshipToMerge.getId() != null && relationshipToMerge.getId()
|
|
.equals(targetRes.getId())) {
|
|
targetDatasetRelationships.set(i, relationshipToMerge);
|
|
replaced = true;
|
|
break;
|
|
|
|
}
|
|
}
|
|
if (!replaced) {
|
|
targetDatasetRelationships.add(relationshipToMerge);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void mergeTags(@Nullable List<CkanTag> tagsToMerge, List<CkanTag> targetTags) {
|
|
if (tagsToMerge != null) {
|
|
for (CkanTag tagToMerge : tagsToMerge) {
|
|
boolean replaced = false;
|
|
for (int i = 0; i < targetTags.size(); i++) {
|
|
CkanTag targetRes = targetTags.get(i);
|
|
if (tagToMerge.getId() != null && tagToMerge.getId()
|
|
.equals(targetRes.getId())) {
|
|
targetTags.set(i, tagToMerge);
|
|
replaced = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!replaced) {
|
|
targetTags.add(tagToMerge);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Jackan specific. Patches a dataset on the ckan server using a
|
|
* {@code package_update} call. Todo: this is a temporary solution until we
|
|
* implement new {@code patch} api of CKAN 2.3
|
|
*
|
|
* @param dataset
|
|
* ckan dataset object. Fields set to {@code null} won't be
|
|
* updated on the server. Items present in lists such as
|
|
* {@code resources} or {@code extras} will be added to existing
|
|
* ones on the server. To support this behaviour provided
|
|
* {@code dataset} might be patched with latest metadata from the
|
|
* server prior sending it for update.
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanDataset patchUpdateDataset(CkanDatasetBase dataset) {
|
|
checkNotNull(dataset, "Need a valid dataset!");
|
|
|
|
checkToken("Tried to patch update dataset" + dataset.getName());
|
|
|
|
CkanDataset origDataset = getDataset(dataset.idOrName());
|
|
|
|
// others
|
|
Map<String, Object> newOthers = new HashMap();
|
|
|
|
if (origDataset.getOthers() != null) {
|
|
newOthers.putAll(origDataset.getOthers());
|
|
}
|
|
if (dataset.getOthers() != null) {
|
|
newOthers.putAll(dataset.getOthers());
|
|
}
|
|
dataset.setOthers(newOthers);
|
|
|
|
// extras
|
|
if (dataset.getExtras() == null) {
|
|
dataset.setExtras(origDataset.getExtras());
|
|
} else {
|
|
Map<String, String> newExtras = new HashMap();
|
|
|
|
if (origDataset.getExtras() != null) {
|
|
newExtras.putAll(origDataset.getExtrasAsHashMap());
|
|
}
|
|
if (dataset.getExtras() != null) {
|
|
newExtras.putAll(dataset.getExtrasAsHashMap());
|
|
}
|
|
dataset.setExtras(extrasMapToList(newExtras));
|
|
}
|
|
|
|
// resources
|
|
List<CkanResource> newResources = new ArrayList();
|
|
mergeResources(origDataset.getResources(), newResources);
|
|
mergeResources(dataset.getResources(), newResources);
|
|
dataset.setResources(newResources);
|
|
|
|
// groups
|
|
List<CkanGroup> newGroups = new ArrayList();
|
|
mergeGroups(origDataset.getGroups(), newGroups);
|
|
mergeGroups(dataset.getGroups(), newGroups);
|
|
dataset.setGroups(newGroups);
|
|
|
|
// tags
|
|
List<CkanTag> newTags = new ArrayList();
|
|
mergeTags(origDataset.getTags(), newTags);
|
|
mergeTags(dataset.getTags(), newTags);
|
|
dataset.setTags(newTags);
|
|
|
|
// relationships as subject
|
|
List<CkanDatasetRelationship> newRelationshipsAsSubject = new ArrayList();
|
|
mergeRelationships(origDataset.getRelationshipsAsSubject(), newRelationshipsAsSubject);
|
|
mergeRelationships(dataset.getRelationshipsAsSubject(), newRelationshipsAsSubject);
|
|
dataset.setRelationshipsAsSubject(newRelationshipsAsSubject);
|
|
|
|
// relationships as object
|
|
List<CkanDatasetRelationship> newRelationshipsAsObject = new ArrayList();
|
|
mergeRelationships(origDataset.getRelationshipsAsObject(), newRelationshipsAsObject);
|
|
mergeRelationships(dataset.getRelationshipsAsObject(), newRelationshipsAsObject);
|
|
dataset.setRelationshipsAsObject(newRelationshipsAsObject);
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanDatasetBase.class).writeValueAsString(dataset);
|
|
} catch (IOException ex) {
|
|
throw new JackanException(COULDNT_JSONIZE + dataset.getClass()
|
|
.getSimpleName(),
|
|
ex);
|
|
|
|
}
|
|
|
|
return postHttp(DatasetResponse.class, "/api/3/action/package_update", json,
|
|
ContentType.APPLICATION_JSON).result;
|
|
|
|
}
|
|
|
|
/**
|
|
* Marks a dataset as {@code 'deleted'}.
|
|
*
|
|
* Note this will just set dataset state to
|
|
* {@link eu.trentorise.opendata.jackan.model.CkanState#deleted} and make it
|
|
* inaccessible from the website and api.
|
|
*
|
|
* @param nameOrId
|
|
* either the dataset name (i.e. apple-production) or the the
|
|
* alphanumerical id (i.e. fe507a10-4c49-4b18-8bf6-6705198cfd42)
|
|
*
|
|
* @throws CkanException
|
|
* on error
|
|
* @throws CkanNotFoundException
|
|
* if dataset is not found
|
|
*
|
|
*/
|
|
// todo check if permissions change accessibility from api
|
|
public synchronized void deleteDataset(String nameOrId) {
|
|
checkNotNull(nameOrId, "Need a valid name or id!");
|
|
|
|
checkToken("Tried to delete dataset" + nameOrId);
|
|
|
|
String json = "{\"id\":\"" + nameOrId + "\"}";
|
|
postHttp(CkanResponse.class, "/api/3/action/package_delete", json, ContentType.APPLICATION_JSON);
|
|
}
|
|
|
|
/**
|
|
* Creates CkanOrganization on the server.
|
|
*
|
|
* @param organization
|
|
* requires at least the name or id. Only non-null fields of
|
|
* {@link CkanGroupOrgBase} will be sent to server.
|
|
* @return a new object with the created organization.
|
|
* @throws CkanException
|
|
* on error.
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanOrganization createOrganization(CkanOrganization organization) {
|
|
checkNotNull(organization, "Need a valid " + organization + "!");
|
|
|
|
checkToken("Tried to create organization " + organization.getName());
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanOrganization.class).writeValueAsString(organization);
|
|
} catch (IOException e) {
|
|
throw new CkanException(COULDNT_JSONIZE + organization.getClass()
|
|
.getSimpleName(),
|
|
this, e);
|
|
|
|
}
|
|
return postHttp(OrganizationResponse.class, "/api/3/action/organization_create", json,
|
|
ContentType.APPLICATION_JSON).result;
|
|
}
|
|
|
|
/**
|
|
* Creates CkanGroup on the server.
|
|
*
|
|
* @param group
|
|
* requires at least the name or id. Only non-null fields of
|
|
* {@link CkanGroupOrgBase} will be sent to server.
|
|
* @return a new object with the created group.
|
|
* @throws CkanException
|
|
* on error.
|
|
* @since 0.4.1
|
|
*/
|
|
public synchronized CkanGroup createGroup(CkanGroup group) {
|
|
checkNotNull(group, "Need a valid " + group + "!");
|
|
|
|
checkToken("Tried to create group " + group.idOrName());
|
|
|
|
String json = null;
|
|
try {
|
|
json = getObjectMapperForPosting(CkanGroup.class).writeValueAsString(group);
|
|
} catch (IOException e) {
|
|
throw new CkanException(COULDNT_JSONIZE + group.getClass()
|
|
.getSimpleName(),
|
|
this, e);
|
|
|
|
}
|
|
return postHttp(GroupResponse.class, "/api/3/action/group_create", json, ContentType.APPLICATION_JSON).result;
|
|
}
|
|
|
|
/**
|
|
* Returns the proxy used by the client, usually in a format with
|
|
* address and port like my.own-proxy.org:1234
|
|
*
|
|
* @since 0.4.1
|
|
*/
|
|
@Nullable
|
|
public String getProxy() {
|
|
return proxy;
|
|
}
|
|
|
|
/**
|
|
* Convenience method to create a Builder with provided client to modify.
|
|
* <p>
|
|
* <strong>WARNING:</strong> The passed client will be modified, so
|
|
* <strong> DO NOT </strong> pass an already built client.
|
|
* </p>
|
|
* <p>
|
|
* The builder is not threadsafe and you can use one builder instance to
|
|
* build only one client instance.
|
|
* </p>
|
|
*/
|
|
protected static CkanClient.Builder newBuilder(CkanClient client) {
|
|
return new Builder(client);
|
|
}
|
|
|
|
}
|
|
|
|
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 TagResponse extends CkanResponse {
|
|
|
|
public CkanTag result;
|
|
}
|
|
|
|
class VocabularyResponse extends CkanResponse {
|
|
|
|
public CkanVocabulary 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;
|
|
}
|