Java tutorial
/* * Copyright 2013-2015 Erudika. http://erudika.com * * 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. * * For issues and patches go to: https://github.com/erudika */ package com.erudika.para.client; import com.erudika.para.core.App; import com.erudika.para.core.ParaObject; import com.erudika.para.core.ParaObjectUtils; import com.erudika.para.core.Tag; import com.erudika.para.core.User; import com.erudika.para.rest.GenericExceptionMapper; import com.erudika.para.rest.Signer; import com.erudika.para.utils.Config; import com.erudika.para.utils.Pager; import com.erudika.para.utils.Utils; import com.erudika.para.validation.Constraint; import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import javax.net.ssl.SSLContext; import static javax.ws.rs.HttpMethod.*; import javax.ws.rs.WebApplicationException; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.HttpUrlConnectorProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Java REST client for communicating with a Para API server. * @author Alex Bogdanovski [alex@erudika.com] */ public final class ParaClient { private static final Logger logger = LoggerFactory.getLogger(ParaClient.class); private static final String DEFAULT_ENDPOINT = "https://paraio.com"; private static final String DEFAULT_PATH = "/v1/"; private static final String JWT_PATH = "/jwt_auth"; private String endpoint; private String path; private String accessKey; private String secretKey; private String tokenKey; private Long tokenKeyExpires; private Long tokenKeyNextRefresh; private Client apiClient; private final Signer signer = new Signer(); public ParaClient(String accessKey, String secretKey) { this.accessKey = accessKey; this.secretKey = secretKey; if (StringUtils.length(secretKey) < 6) { logger.warn("Secret key appears to be invalid. Make sure you call 'signIn()' first."); } ClientConfig clientConfig = new ClientConfig(); clientConfig.register(GenericExceptionMapper.class); clientConfig.register(new JacksonJsonProvider(ParaObjectUtils.getJsonMapper())); clientConfig.connectorProvider(new HttpUrlConnectorProvider().useSetMethodWorkaround()); SSLContext sslContext = SslConfigurator.newInstance().securityProtocol("TLSv1.2").createSSLContext(); apiClient = ClientBuilder.newBuilder().sslContext(sslContext).withConfig(clientConfig).build(); } protected Client getApiClient() { return apiClient; } protected void setApiClient(Client apiClient) { this.apiClient = apiClient; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } /** * Closes the underlying Jersey client and releases resources. */ public void close() { if (apiClient != null) { apiClient.close(); } } /** * Returns the {@link App} for the current access key (appid). * @return the App object */ public App getApp() { return me(); } /** * Returns the endpoint URL * @return the endpoint */ public String getEndpoint() { if (StringUtils.isBlank(endpoint)) { return DEFAULT_ENDPOINT; } else { return endpoint; } } /** * Sets the API request path * @param path a new path */ public void setApiPath(String path) { this.path = path; } /** * Returns the API request path * @return the request path without parameters */ public String getApiPath() { if (StringUtils.isBlank(path)) { return DEFAULT_PATH; } else { if (!path.endsWith("/")) { path += "/"; } return path; } } /** * @return the JWT access token, or null if not signed in */ public String getAccessToken() { return tokenKey; } /** * Sets the JWT access token. * @param token a valid token */ @SuppressWarnings("unchecked") public void setAccessToken(String token) { if (!StringUtils.isBlank(token)) { try { String payload = Utils.base64dec(StringUtils.substringBetween(token, ".", ".")); Map<String, Object> decoded = ParaObjectUtils.getJsonMapper().readValue(payload, Map.class); if (decoded != null && decoded.containsKey("exp")) { this.tokenKeyExpires = (Long) decoded.get("exp"); this.tokenKeyNextRefresh = (Long) decoded.get("refresh"); } } catch (Exception ex) { this.tokenKeyExpires = null; this.tokenKeyNextRefresh = null; } } this.tokenKey = token; } /** * Clears the JWT token from memory, if such exists. */ private void clearAccessToken() { tokenKey = null; tokenKeyExpires = null; tokenKeyNextRefresh = null; } private String key(boolean refresh) { if (tokenKey != null) { if (refresh) { refreshToken(); } return "Bearer " + tokenKey; } return secretKey; } @SuppressWarnings("unchecked") private <T> T getEntity(Response res, Class<?> type) { if (res != null) { if (res.getStatus() == Response.Status.OK.getStatusCode() || res.getStatus() == Response.Status.CREATED.getStatusCode() || res.getStatus() == Response.Status.NOT_MODIFIED.getStatusCode()) { return res.hasEntity() ? res.readEntity((Class<T>) type) : null; } else if (res.getStatus() != Response.Status.NOT_FOUND.getStatusCode() && res.getStatus() != Response.Status.NOT_MODIFIED.getStatusCode() && res.getStatus() != Response.Status.NO_CONTENT.getStatusCode()) { Map<String, Object> error = res.hasEntity() ? res.readEntity(Map.class) : null; if (error != null && error.containsKey("code")) { String msg = error.containsKey("message") ? (String) error.get("message") : "error"; logger.error(msg + " - {}", new WebApplicationException((Integer) error.get("code")).getMessage()); } } } return null; } private String getFullPath(String resourcePath) { if (StringUtils.startsWith(resourcePath, JWT_PATH)) { return resourcePath; } if (resourcePath == null) { resourcePath = ""; } else if (resourcePath.startsWith("/")) { resourcePath = resourcePath.substring(1); } return getApiPath() + resourcePath; } private Response invokeGet(String resourcePath, MultivaluedMap<String, String> params) { return signer.invokeSignedRequest(getApiClient(), accessKey, key(!JWT_PATH.equals(resourcePath)), GET, getEndpoint(), getFullPath(resourcePath), null, params, new byte[0]); } private Response invokePost(String resourcePath, Entity<?> entity) { return signer.invokeSignedRequest(getApiClient(), accessKey, key(false), POST, getEndpoint(), getFullPath(resourcePath), null, null, entity); } private Response invokePut(String resourcePath, Entity<?> entity) { return signer.invokeSignedRequest(getApiClient(), accessKey, key(false), PUT, getEndpoint(), getFullPath(resourcePath), null, null, entity); } private Response invokePatch(String resourcePath, Entity<?> entity) { return signer.invokeSignedRequest(getApiClient(), accessKey, key(false), "PATCH", getEndpoint(), getFullPath(resourcePath), null, null, entity); } private Response invokeDelete(String resourcePath, MultivaluedMap<String, String> params) { return signer.invokeSignedRequest(getApiClient(), accessKey, key(false), DELETE, getEndpoint(), getFullPath(resourcePath), null, params, new byte[0]); } private MultivaluedMap<String, String> pagerToParams(Pager... pager) { MultivaluedMap<String, String> map = new MultivaluedHashMap<String, String>(); if (pager != null && pager.length > 0) { Pager p = pager[0]; if (p != null) { map.put("page", Collections.singletonList(Long.toString(p.getPage()))); map.put("desc", Collections.singletonList(Boolean.toString(p.isDesc()))); map.put("limit", Collections.singletonList(Integer.toString(p.getLimit()))); if (p.getSortby() != null) { map.put("sort", Collections.singletonList(p.getSortby())); } } } return map; } @SuppressWarnings("unchecked") private <P extends ParaObject> List<P> getItemsFromList(List<?> result) { if (result != null && !result.isEmpty()) { // this isn't very efficient but there's no way to know what type of objects we're reading ArrayList<P> objects = new ArrayList<P>(result.size()); for (Object map : result) { P p = ParaObjectUtils.setAnnotatedFields((Map<String, Object>) map); if (p != null) { objects.add(p); } } return objects; } return Collections.emptyList(); } @SuppressWarnings("unchecked") private <P extends ParaObject> List<P> getItems(Map<String, Object> result, Pager... pager) { if (result != null && !result.isEmpty() && result.containsKey("items")) { if (pager != null && pager.length > 0 && pager[0] != null && result.containsKey("totalHits")) { pager[0].setCount(((Integer) result.get("totalHits")).longValue()); } return (List<P>) getItemsFromList((List<?>) result.get("items")); } return Collections.emptyList(); } ///////////////////////////////////////////// // PERSISTENCE ///////////////////////////////////////////// /** * Persists an object to the data store. If the object's type and id are given, * then the request will be a {@code PUT} request and any existing object will be * overwritten. * @param <P> the type of object * @param obj the domain object * @return the same object with assigned id or null if not created. */ public <P extends ParaObject> P create(P obj) { if (obj == null) { return null; } if (StringUtils.isBlank(obj.getId()) || StringUtils.isBlank(obj.getType())) { return getEntity(invokePost(obj.getType(), Entity.json(obj)), obj.getClass()); } else { return getEntity(invokePut(obj.getType().concat("/").concat(obj.getId()), Entity.json(obj)), obj.getClass()); } } /** * Retrieves an object from the data store. * @param <P> the type of object * @param type the type of the object * @param id the id of the object * @return the retrieved object or null if not found */ public <P extends ParaObject> P read(String type, String id) { if (StringUtils.isBlank(type) || StringUtils.isBlank(id)) { return null; } return getEntity(invokeGet(type.concat("/").concat(id), null), ParaObjectUtils.toClass(type)); } /** * Retrieves an object from the data store. * @param <P> the type of object * @param id the id of the object * @return the retrieved object or null if not found */ public <P extends ParaObject> P read(String id) { if (StringUtils.isBlank(id)) { return null; } Map<String, Object> data = getEntity(invokeGet("_id/".concat(id), null), Map.class); return ParaObjectUtils.setAnnotatedFields(data); } /** * Updates an object permanently. Supports partial updates. * @param <P> the type of object * @param obj the object to update * @return the updated object */ public <P extends ParaObject> P update(P obj) { if (obj == null) { return null; } return getEntity(invokePatch(obj.getObjectURI(), Entity.json(obj)), obj.getClass()); } /** * Deletes an object permanently. * @param <P> the type of object * @param obj the object */ public <P extends ParaObject> void delete(P obj) { if (obj == null) { return; } invokeDelete(obj.getObjectURI(), null); } /** * Saves multiple objects to the data store. * @param <P> the type of object * @param objects the list of objects to save * @return a list of objects */ public <P extends ParaObject> List<P> createAll(List<P> objects) { if (objects == null || objects.isEmpty() || objects.get(0) == null) { return Collections.emptyList(); } return getItemsFromList((List<?>) getEntity(invokePost("_batch", Entity.json(objects)), List.class)); } /** * Retrieves multiple objects from the data store. * @param <P> the type of object * @param keys a list of object ids * @return a list of objects */ public <P extends ParaObject> List<P> readAll(List<String> keys) { if (keys == null || keys.isEmpty()) { return Collections.emptyList(); } MultivaluedMap<String, String> ids = new MultivaluedHashMap<String, String>(); ids.put("ids", keys); return getItemsFromList((List<?>) getEntity(invokeGet("_batch", ids), List.class)); } /** * Updates multiple objects. * @param <P> the type of object * @param objects the objects to update * @return a list of objects */ public <P extends ParaObject> List<P> updateAll(List<P> objects) { if (objects == null || objects.isEmpty()) { return Collections.emptyList(); } return getItemsFromList((List<?>) getEntity(invokePatch("_batch", Entity.json(objects)), List.class)); } /** * Deletes multiple objects. * @param <P> the type of object * @param keys the ids of the objects to delete */ public <P extends ParaObject> void deleteAll(List<String> keys) { if (keys == null || keys.isEmpty()) { return; } MultivaluedMap<String, String> ids = new MultivaluedHashMap<String, String>(); ids.put("ids", keys); invokeDelete("_batch", ids); } /** * Returns a list all objects found for the given type. * The result is paginated so only one page of items is returned, at a time. * @param <P> the type of object * @param type the type of objects to search for * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects */ @SuppressWarnings("unchecked") public <P extends ParaObject> List<P> list(String type, Pager... pager) { if (StringUtils.isBlank(type)) { return Collections.emptyList(); } return getItems((Map<String, Object>) getEntity(invokeGet(type, pagerToParams(pager)), Map.class), pager); } ///////////////////////////////////////////// // SEARCH ///////////////////////////////////////////// /** * Simple id search. * @param <P> type of the object * @param id the id * @return the object if found or null */ @SuppressWarnings("unchecked") public <P extends ParaObject> P findById(String id) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle(Config._ID, id); List<P> list = getItems(find("id", params)); return list.isEmpty() ? null : list.get(0); } /** * Simple multi id search. * @param <P> type of the object * @param ids a list of ids to search for * @return a list of object found */ @SuppressWarnings("unchecked") public <P extends ParaObject> List<P> findByIds(List<String> ids) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.put("ids", ids); return (List<P>) getItems(find("ids", params)); } /** * Search for {@link com.erudika.para.core.Address} objects in a radius of X km from a given point. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param query the query string * @param radius the radius of the search circle * @param lat latitude * @param lng longitude * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findNearby(String type, String query, int radius, double lat, double lng, Pager... pager) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("latlng", lat + "," + lng); params.putSingle("radius", Integer.toString(radius)); params.putSingle("q", query); params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("nearby", params), pager); } /** * Searches for objects that have a property which value starts with a given prefix. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param field the property name of an object * @param prefix the prefix * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findPrefix(String type, String field, String prefix, Pager... pager) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("field", field); params.putSingle("prefix", prefix); params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("prefix", params), pager); } /** * Simple query string search. This is the basic search method. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param query the query string * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findQuery(String type, String query, Pager... pager) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("q", query); params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("", params), pager); } /** * Searches for objects that have similar property values to a given text. A "find like this" query. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param filterKey exclude an object with this key from the results (optional) * @param fields a list of property names * @param liketext text to compare to * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findSimilar(String type, String filterKey, String[] fields, String liketext, Pager... pager) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.put("fields", fields == null ? null : Arrays.asList(fields)); params.putSingle("filterid", filterKey); params.putSingle("like", liketext); params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("similar", params), pager); } /** * Searches for objects tagged with one or more tags. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param tags the list of tags * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findTagged(String type, String[] tags, Pager... pager) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.put("tags", tags == null ? null : Arrays.asList(tags)); params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("tagged", params), pager); } /** * Searches for {@link com.erudika.para.core.Tag} objects. * This method might be deprecated in the future. * @param <P> type of the object * @param keyword the tag keyword to search for * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findTags(String keyword, Pager... pager) { keyword = (keyword == null) ? "*" : keyword.concat("*"); return findWildcard(Utils.type(Tag.class), "tag", keyword, pager); } /** * Searches for objects having a property value that is in list of possible values. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param field the property name of an object * @param terms a list of terms (property values) * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findTermInList(String type, String field, List<String> terms, Pager... pager) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("field", field); params.put("terms", terms); params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("in", params), pager); } /** * Searches for objects that have properties matching some given values. A terms query. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param terms a map of fields (property names) to terms (property values) * @param matchAll match all terms. If true - AND search, if false - OR search * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findTerms(String type, Map<String, ?> terms, boolean matchAll, Pager... pager) { if (terms == null) { return Collections.emptyList(); } MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("matchall", Boolean.toString(matchAll)); LinkedList<String> list = new LinkedList<String>(); for (Map.Entry<String, ? extends Object> term : terms.entrySet()) { String key = term.getKey(); Object value = term.getValue(); if (value != null) { list.add(key.concat(Config.SEPARATOR).concat(value.toString())); } } if (!terms.isEmpty()) { params.put("terms", list); } params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("terms", params), pager); } /** * Searches for objects that have a property with a value matching a wildcard query. * @param <P> type of the object * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param field the property name of an object * @param wildcard wildcard query string. For example "cat*". * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of objects found */ public <P extends ParaObject> List<P> findWildcard(String type, String field, String wildcard, Pager... pager) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("field", field); params.putSingle("q", wildcard); params.putSingle(Config._TYPE, type); params.putAll(pagerToParams(pager)); return getItems(find("wildcard", params), pager); } /** * Counts indexed objects. * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @return the number of results found */ public Long getCount(String type) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle(Config._TYPE, type); Pager pager = new Pager(); getItems(find("count", params), pager); return pager.getCount(); } /** * Counts indexed objects matching a set of terms/values. * @param type the type of object to search for. See {@link com.erudika.para.core.ParaObject#getType()} * @param terms a list of terms (property values) * @return the number of results found */ public Long getCount(String type, Map<String, ?> terms) { if (terms == null) { return 0L; } MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); LinkedList<String> list = new LinkedList<String>(); for (Map.Entry<String, ? extends Object> term : terms.entrySet()) { String key = term.getKey(); Object value = term.getValue(); if (value != null) { list.add(key.concat(Config.SEPARATOR).concat(value.toString())); } } if (!terms.isEmpty()) { params.put("terms", list); } params.putSingle(Config._TYPE, type); params.putSingle("count", "true"); Pager pager = new Pager(); getItems(find("terms", params), pager); return pager.getCount(); } private <P extends ParaObject> Map<String, Object> find(String queryType, MultivaluedMap<String, String> params) { Map<String, Object> map = new HashMap<String, Object>(); if (params != null && !params.isEmpty()) { String qType = StringUtils.isBlank(queryType) ? "" : "/".concat(queryType); return getEntity(invokeGet("search".concat(qType), params), Map.class); } else { map.put("items", Collections.emptyList()); map.put("totalHits", 0); } return map; } ///////////////////////////////////////////// // LINKS ///////////////////////////////////////////// /** * Count the total number of links between this object and another type of object. * @param type2 the other type of object * @param obj the object to execute this method on * @return the number of links for the given object */ @SuppressWarnings("unchecked") public Long countLinks(ParaObject obj, String type2) { if (obj == null || obj.getId() == null || type2 == null) { return 0L; } MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("count", "true"); Pager pager = new Pager(); String url = Utils.formatMessage("{0}/links/{1}", obj.getObjectURI(), type2); getItems((Map<String, Object>) getEntity(invokeGet(url, params), Map.class), pager); return pager.getCount(); } /** * Returns all objects linked to the given one. Only applicable to many-to-many relationships. * @param <P> type of linked objects * @param type2 type of linked objects to search for * @param obj the object to execute this method on * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of linked objects */ @SuppressWarnings("unchecked") public <P extends ParaObject> List<P> getLinkedObjects(ParaObject obj, String type2, Pager... pager) { if (obj == null || obj.getId() == null || type2 == null) { return Collections.emptyList(); } String url = Utils.formatMessage("{0}/links/{1}", obj.getObjectURI(), type2); return getItems((Map<String, Object>) getEntity(invokeGet(url, null), Map.class), pager); } /** * Checks if this object is linked to another. * @param type2 the other type * @param id2 the other id * @param obj the object to execute this method on * @return true if the two are linked */ public boolean isLinked(ParaObject obj, String type2, String id2) { if (obj == null || obj.getId() == null || type2 == null || id2 == null) { return false; } String url = Utils.formatMessage("{0}/links/{1}/{2}", obj.getObjectURI(), type2, id2); return getEntity(invokeGet(url, null), Boolean.class); } /** * Checks if a given object is linked to this one. * @param toObj the other object * @param obj the object to execute this method on * @return true if linked */ public boolean isLinked(ParaObject obj, ParaObject toObj) { if (obj == null || obj.getId() == null || toObj == null || toObj.getId() == null) { return false; } return isLinked(obj, toObj.getType(), toObj.getId()); } /** * Links an object to this one in a many-to-many relationship. * Only a link is created. Objects are left untouched. * The type of the second object is automatically determined on read. * @param id2 link to the object with this id * @param obj the object to execute this method on * @return the id of the {@link com.erudika.para.core.Linker} object that is created */ public String link(ParaObject obj, String id2) { if (obj == null || obj.getId() == null || id2 == null) { return null; } String url = Utils.formatMessage("{0}/links/{1}", obj.getObjectURI(), id2); return getEntity(invokePost(url, null), String.class); } /** * Unlinks an object from this one. * Only a link is deleted. Objects are left untouched. * @param type2 the other type * @param obj the object to execute this method on * @param id2 the other id */ public void unlink(ParaObject obj, String type2, String id2) { if (obj == null || obj.getId() == null || type2 == null || id2 == null) { return; } String url = Utils.formatMessage("{0}/links/{1}/{2}", obj.getObjectURI(), type2, id2); invokeDelete(url, null); } /** * Unlinks all objects that are linked to this one. * @param obj the object to execute this method on * Only {@link com.erudika.para.core.Linker} objects are deleted. * {@link com.erudika.para.core.ParaObject}s are left untouched. */ public void unlinkAll(ParaObject obj) { if (obj == null || obj.getId() == null) { return; } String url = Utils.formatMessage("{0}/links", obj.getObjectURI()); invokeDelete(url, null); } /** * Count the total number of child objects for this object. * @param type2 the type of the other object * @param obj the object to execute this method on * @return the number of links */ @SuppressWarnings("unchecked") public Long countChildren(ParaObject obj, String type2) { if (obj == null || obj.getId() == null || type2 == null) { return 0L; } MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("count", "true"); params.putSingle("childrenonly", "true"); Pager pager = new Pager(); String url = Utils.formatMessage("{0}/links/{1}", obj.getObjectURI(), type2); getItems((Map<String, Object>) getEntity(invokeGet(url, params), Map.class), pager); return pager.getCount(); } /** * Returns all child objects linked to this object. * @param <P> the type of children * @param type2 the type of children to look for * @param obj the object to execute this method on * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of {@link ParaObject} in a one-to-many relationship with this object */ @SuppressWarnings("unchecked") public <P extends ParaObject> List<P> getChildren(ParaObject obj, String type2, Pager... pager) { if (obj == null || obj.getId() == null || type2 == null) { return Collections.emptyList(); } MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("childrenonly", "true"); String url = Utils.formatMessage("{0}/links/{1}", obj.getObjectURI(), type2); return getItems((Map<String, Object>) getEntity(invokeGet(url, params), Map.class), pager); } /** * Returns all child objects linked to this object. * @param <P> the type of children * @param type2 the type of children to look for * @param field the field name to use as filter * @param term the field value to use as filter * @param obj the object to execute this method on * @param pager a {@link com.erudika.para.utils.Pager} * @return a list of {@link ParaObject} in a one-to-many relationship with this object */ @SuppressWarnings("unchecked") public <P extends ParaObject> List<P> getChildren(ParaObject obj, String type2, String field, String term, Pager... pager) { if (obj == null || obj.getId() == null || type2 == null) { return Collections.emptyList(); } MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("childrenonly", "true"); params.putSingle("field", field); params.putSingle("term", term); String url = Utils.formatMessage("{0}/links/{1}", obj.getObjectURI(), type2); return getItems((Map<String, Object>) getEntity(invokeGet(url, params), Map.class), pager); } /** * Deletes all child objects permanently. * @param obj the object to execute this method on * @param type2 the children's type. */ public void deleteChildren(ParaObject obj, String type2) { if (obj == null || obj.getId() == null || type2 == null) { return; } MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("childrenonly", "true"); String url = Utils.formatMessage("{0}/links/{1}", obj.getObjectURI(), type2); invokeDelete(url, params); } ///////////////////////////////////////////// // UTILS ///////////////////////////////////////////// /** * Generates a new unique id. * @return a new id */ public String newId() { String res = getEntity(invokeGet("utils/newid", null), String.class); return res != null ? res : ""; } /** * Returns the current timestamp. * @return a long number */ public long getTimestamp() { Long res = getEntity(invokeGet("utils/timestamp", null), Long.class); return res != null ? res : 0L; } /** * Formats a date in a specific format. * @param format the date format * @param loc the locale instance * @return a formatted date */ public String formatDate(String format, Locale loc) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("format", format); params.putSingle("locale", loc == null ? null : loc.toString()); return getEntity(invokeGet("utils/formatdate", params), String.class); } /** * Converts spaces to dashes. * @param str a string with spaces * @param replaceWith a string to replace spaces with * @return a string with dashes */ public String noSpaces(String str, String replaceWith) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("string", str); params.putSingle("replacement", replaceWith); return getEntity(invokeGet("utils/nospaces", params), String.class); } /** * Strips all symbols, punctuation, whitespace and control chars from a string. * @param str a dirty string * @return a clean string */ public String stripAndTrim(String str) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("string", str); return getEntity(invokeGet("utils/nosymbols", params), String.class); } /** * Converts Markdown to HTML * @param markdownString Markdown * @return HTML */ public String markdownToHtml(String markdownString) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("md", markdownString); return getEntity(invokeGet("utils/md2html", params), String.class); } /** * Returns the number of minutes, hours, months elapsed for a time delta (milliseconds). * @param delta the time delta between two events, in milliseconds * @return a string like "5m", "1h" */ public String approximately(long delta) { MultivaluedMap<String, String> params = new MultivaluedHashMap<String, String>(); params.putSingle("delta", Long.toString(delta)); return getEntity(invokeGet("utils/timeago", params), String.class); } ///////////////////////////////////////////// // MISC ///////////////////////////////////////////// /** * Generates a new set of access/secret keys. * Old keys are discarded and invalid after this. * @return a map of new credentials */ public Map<String, String> newKeys() { Map<String, String> keys = getEntity(invokePost("_newkeys", null), Map.class); if (keys != null && keys.containsKey("secretKey")) { this.secretKey = keys.get("secretKey"); } return keys; } /** * Returns all registered types for this App. * @return a map of plural-singular form of all the registered types. */ public Map<String, String> types() { return getEntity(invokeGet("_types", null), Map.class); } /** * Returns a {@link com.erudika.para.core.User} or an * {@link com.erudika.para.core.App} that is currently authenticated. * @param <P> an App or User * @return a {@link com.erudika.para.core.User} or an {@link com.erudika.para.core.App} */ public <P extends ParaObject> P me() { Map<String, Object> data = getEntity(invokeGet("_me", null), Map.class); return ParaObjectUtils.setAnnotatedFields(data); } ///////////////////////////////////////////// // Validation Constraints ///////////////////////////////////////////// /** * Returns the validation constraints map. * @return a map containing all validation constraints. */ public Map<String, Map<String, Map<String, Map<String, ?>>>> validationConstraints() { return getEntity(invokeGet("_constraints", null), Map.class); } /** * Returns the validation constraints map. * @param type a type * @return a map containing all validation constraints for this type. */ public Map<String, Map<String, Map<String, Map<String, ?>>>> validationConstraints(String type) { return getEntity(invokeGet(Utils.formatMessage("_constraints/{0}", type), null), Map.class); } /** * Add a new constraint for a given field. * @param type a type * @param field a field name * @param c the constraint * @return a map containing all validation constraints for this type. */ public Map<String, Map<String, Map<String, Map<String, ?>>>> addValidationConstraint(String type, String field, Constraint c) { if (StringUtils.isBlank(type) || StringUtils.isBlank(field) || c == null) { return Collections.emptyMap(); } return getEntity(invokePut(Utils.formatMessage("_constraints/{0}/{1}/{2}", type, field, c.getName()), Entity.json(c.getPayload())), Map.class); } /** * Removes a validation constraint for a given field. * @param type a type * @param field a field name * @param constraintName the name of the constraint to remove * @return a map containing all validation constraints for this type. */ public Map<String, Map<String, Map<String, Map<String, ?>>>> removeValidationConstraint(String type, String field, String constraintName) { if (StringUtils.isBlank(type) || StringUtils.isBlank(field) || StringUtils.isBlank(constraintName)) { return Collections.emptyMap(); } return getEntity( invokeDelete(Utils.formatMessage("_constraints/{0}/{1}/{2}", type, field, constraintName), null), Map.class); } ///////////////////////////////////////////// // Resource Permissions ///////////////////////////////////////////// /** * Returns the permissions for all subjects and resources for current app. * @return a map of subject ids to resource names to a list of allowed methods */ public Map<String, Map<String, List<String>>> resourcePermissions() { return getEntity(invokeGet("_permissions", null), Map.class); } /** * Returns only the permissions for a given subject (user) of the current app. * @param subjectid the subject id (user id) * @return a map of subject ids to resource names to a list of allowed methods */ public Map<String, Map<String, List<String>>> resourcePermissions(String subjectid) { return getEntity(invokeGet(Utils.formatMessage("_permissions/{0}", subjectid), null), Map.class); } /** * Grants a permission to a subject that allows them to call the specified HTTP methods on a given resource. * @param subjectid subject id (user id) * @param resourceName resource name or object type * @param permission a set of HTTP methods * @return a map of the permissions for this subject id */ public Map<String, Map<String, List<String>>> grantResourcePermission(String subjectid, String resourceName, EnumSet<App.AllowedMethods> permission) { if (StringUtils.isBlank(subjectid) || StringUtils.isBlank(resourceName) || permission == null) { return Collections.emptyMap(); } return getEntity(invokePut(Utils.formatMessage("_permissions/{0}/{1}", subjectid, resourceName), Entity.json(permission)), Map.class); } /** * Revokes a permission for a subject, meaning they no longer will be able to access the given resource. * @param subjectid subject id (user id) * @param resourceName resource name or object type * @return a map of the permissions for this subject id */ public Map<String, Map<String, List<String>>> revokeResourcePermission(String subjectid, String resourceName) { if (StringUtils.isBlank(subjectid) || StringUtils.isBlank(resourceName)) { return Collections.emptyMap(); } return getEntity(invokeDelete(Utils.formatMessage("_permissions/{0}/{1}", subjectid, resourceName), null), Map.class); } /** * Revokes all permission for a subject. * @param subjectid subject id (user id) * @return a map of the permissions for this subject id */ public Map<String, Map<String, List<String>>> revokeAllResourcePermissions(String subjectid) { if (StringUtils.isBlank(subjectid)) { return Collections.emptyMap(); } return getEntity(invokeDelete(Utils.formatMessage("_permissions/{0}", subjectid), null), Map.class); } /** * Checks if a subject is allowed to call method X on resource Y. * @param subjectid subject id * @param resourceName resource name (type) * @param httpMethod HTTP method name * @return true if allowed */ public boolean isAllowedTo(String subjectid, String resourceName, String httpMethod) { if (StringUtils.isBlank(subjectid) || StringUtils.isBlank(resourceName) || StringUtils.isBlank(httpMethod)) { return false; } String url = Utils.formatMessage("_permissions/{0}/{1}/{2}", subjectid, resourceName, httpMethod); return getEntity(invokeGet(url, null), Boolean.class); } ///////////////////////////////////////////// // Access Tokens ///////////////////////////////////////////// /** * Takes an identity provider access token and fetches the user data from that provider. * A new {@link User} object is created if that user doesn't exist. * Access tokens are returned upon successful authentication using one of the SDKs from * Facebook, Google, Twitter, etc. * <b>Note:</b> Twitter uses OAuth 1 and gives you a token and a token secret. * <b>You must concatenate them like this: <code>{oauth_token}:{oauth_token_secret}</code> and * use that as the provider access token.</b> * @param provider identity provider, e.g. 'facebook', 'google'... * @param providerToken access token from a provider like Facebook, Google, Twitter * @return a {@link User} object or null if something failed */ @SuppressWarnings("unchecked") public User signIn(String provider, String providerToken) { if (!StringUtils.isBlank(provider) && !StringUtils.isBlank(providerToken)) { Map<String, String> credentials = new HashMap<String, String>(); credentials.put("appid", accessKey); credentials.put("provider", provider); credentials.put("token", providerToken); Map<String, Object> result = getEntity(invokePost(JWT_PATH, Entity.json(credentials)), Map.class); if (result != null && result.containsKey("user") && result.containsKey("jwt")) { Map<?, ?> jwtData = (Map<?, ?>) result.get("jwt"); Map<String, Object> userData = (Map<String, Object>) result.get("user"); tokenKey = (String) jwtData.get("access_token"); tokenKeyExpires = (Long) jwtData.get("expires"); tokenKeyNextRefresh = (Long) jwtData.get("refresh"); return ParaObjectUtils.setAnnotatedFields(userData); } else { clearAccessToken(); } } return null; } /** * Clears the JWT access token but token is not revoked. * Tokens can be revoked globally per user with {@link #revokeAllTokens()}. */ public void signOut() { clearAccessToken(); } /** * Refreshes the JWT access token. This requires a valid existing token. * Call {@link #signIn(java.lang.String, java.lang.String)} first. * @return true if token was refreshed */ protected boolean refreshToken() { long now = System.currentTimeMillis(); boolean notExpired = tokenKeyExpires != null && tokenKeyExpires > now; boolean canRefresh = tokenKeyNextRefresh != null && (tokenKeyNextRefresh < now || tokenKeyNextRefresh > tokenKeyExpires); // token present and NOT expired if (tokenKey != null && notExpired && canRefresh) { Map<String, Object> result = getEntity(invokeGet(JWT_PATH, null), Map.class); if (result != null && result.containsKey("user") && result.containsKey("jwt")) { Map<?, ?> jwtData = (Map<?, ?>) result.get("jwt"); tokenKey = (String) jwtData.get("access_token"); tokenKeyExpires = (Long) jwtData.get("expires"); tokenKeyNextRefresh = (Long) jwtData.get("refresh"); return true; } else { clearAccessToken(); } } return false; } /** * Revokes all user tokens for a given user id. * This would be equivalent to "logout everywhere". * <b>Note:</b> Generating a new API secret on the server will also invalidate all client tokens. * Requires a valid existing token. * @return true if successful */ public boolean revokeAllTokens() { return getEntity(invokeDelete(JWT_PATH, null), Map.class) != null; } }