diff --git a/cloudinary-core/src/main/java/com/cloudinary/Api.java b/cloudinary-core/src/main/java/com/cloudinary/Api.java index d81dcd9d..c29fdad5 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Api.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Api.java @@ -5,6 +5,8 @@ import com.cloudinary.api.ApiResponse; import com.cloudinary.api.AuthorizationRequired; import com.cloudinary.api.exceptions.*; +import com.cloudinary.metadata.MetadataField; +import com.cloudinary.metadata.MetadataDataSource; import com.cloudinary.strategies.AbstractApiStrategy; import com.cloudinary.utils.ObjectUtils; import com.cloudinary.utils.StringUtils; @@ -15,6 +17,7 @@ public class Api { public enum HttpMethod {GET, POST, PUT, DELETE;} + public final static Map> CLOUDINARY_API_ERROR_CLASSES = new HashMap>(); static { @@ -30,6 +33,7 @@ public enum HttpMethod {GET, POST, PUT, DELETE;} public final Cloudinary cloudinary; private AbstractApiStrategy strategy; + protected ApiResponse callApi(HttpMethod method, Iterable uri, Map params, Map options) throws Exception { return this.strategy.callApi(method, uri, params, options); } @@ -78,18 +82,18 @@ public ApiResponse resourcesByTag(String tag, Map options) throws Exception { } public ApiResponse resourcesByContext(String key, Map options) throws Exception { - return resourcesByContext(key,null,options); + return resourcesByContext(key, null, options); } - public ApiResponse resourcesByContext(String key,String value, Map options) throws Exception { + public ApiResponse resourcesByContext(String key, String value, Map options) throws Exception { if (options == null) options = ObjectUtils.emptyMap(); String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); Map params = ObjectUtils.only(options, "next_cursor", "direction", "max_results", "tags", "context", "moderations"); - params.put("key",key); + params.put("key", key); if (StringUtils.isNotBlank(value)) { - params.put("value",value); + params.put("value", value); } - return callApi(HttpMethod.GET, Arrays.asList("resources", resourceType,"context"), params , options); + return callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, "context"), params, options); } public ApiResponse resourcesByIds(Iterable publicIds, Map options) throws Exception { @@ -372,7 +376,8 @@ public ApiResponse createStreamingProfile(String name, String displayName, List< /** * Get a streaming profile information - * @param name the name of the profile to fetch + * + * @param name the name of the profile to fetch * @param options additional options * @return a streaming profile * @throws Exception an exception @@ -395,6 +400,7 @@ public ApiResponse getStreamingProfile(String name) throws Exception { /** * List Streaming profiles + * * @param options additional options * @return a list of all streaming profiles defined for the current cloud * @throws Exception an exception @@ -416,7 +422,8 @@ public ApiResponse listStreamingProfiles() throws Exception { /** * Delete a streaming profile information. Predefined profiles are restored to the default setting. - * @param name the name of the profile to delete + * + * @param name the name of the profile to delete * @param options additional options * @return a streaming profile * @throws Exception an exception @@ -481,11 +488,11 @@ public ApiResponse updateStreamingProfile(String name, String displayName, List< * @param accessMode The new access mode, "public" or "authenticated" * @param prefix The prefix by which to filter applicable resources * @param options additional options - *
    - *
  • resource_type - (default "image") - the type of resources to modify
  • - *
  • max_results - optional - the maximum resources to process in a single invocation
  • - *
  • next_cursor - optional - provided by a previous call to the method
  • - *
+ *
    + *
  • resource_type - (default "image") - the type of resources to modify
  • + *
  • max_results - optional - the maximum resources to process in a single invocation
  • + *
  • next_cursor - optional - provided by a previous call to the method
  • + *
* @return a map of the returned values *
    *
  • updated - an array of resources
  • @@ -503,11 +510,11 @@ public ApiResponse updateResourcesAccessModeByPrefix(String accessMode, String p * @param accessMode The new access mode, "public" or "authenticated" * @param tag The tag by which to filter applicable resources * @param options additional options - *
      - *
    • resource_type - (default "image") - the type of resources to modify
    • - *
    • max_results - optional - the maximum resources to process in a single invocation
    • - *
    • next_cursor - optional - provided by a previous call to the method
    • - *
    + *
      + *
    • resource_type - (default "image") - the type of resources to modify
    • + *
    • max_results - optional - the maximum resources to process in a single invocation
    • + *
    • next_cursor - optional - provided by a previous call to the method
    • + *
    * @return a map of the returned values *
      *
    • updated - an array of resources
    • @@ -525,11 +532,11 @@ public ApiResponse updateResourcesAccessModeByTag(String accessMode, String tag, * @param accessMode The new access mode, "public" or "authenticated" * @param publicIds A list of public ids of resources to be updated * @param options additional options - *
        - *
      • resource_type - (default "image") - the type of resources to modify
      • - *
      • max_results - optional - the maximum resources to process in a single invocation
      • - *
      • next_cursor - optional - provided by a previous call to the method
      • - *
      + *
        + *
      • resource_type - (default "image") - the type of resources to modify
      • + *
      • max_results - optional - the maximum resources to process in a single invocation
      • + *
      • next_cursor - optional - provided by a previous call to the method
      • + *
      * @return a map of the returned values *
        *
      • updated - an array of resources
      • @@ -552,4 +559,81 @@ private ApiResponse updateResourcesAccessMode(String accessMode, String byKey, O return callApi(HttpMethod.POST, uri, params, options); } + /** + * Add a new metadata field definition + * @param field The field to add. + * @return A map representing the newlay added field. + * @throws Exception + */ + public ApiResponse addMetadataField(MetadataField field) throws Exception { + return callApi(HttpMethod.POST, Collections.singletonList("metadata_fields"), + ObjectUtils.toMap(field), ObjectUtils.asMap ("content_type", "json")); + } + + /** + * List all the metadata field definitions (structure, not values) + * @return A map containing the list of field definitions maps. + * @throws Exception + */ + public ApiResponse listMetadataFields() throws Exception { + return callApi(HttpMethod.GET, Collections.singletonList("metadata_fields"), Collections.emptyMap(), Collections.emptyMap()); + } + + /** + * Get a metadata field definition by id + * @param fieldExternalId The id of the field to retrieve + * @return The fields definitions. + * @throws Exception + */ + public ApiResponse metadataFieldByFieldId(String fieldExternalId) throws Exception { + return callApi(HttpMethod.GET, Arrays.asList("metadata_fields", fieldExternalId), Collections.emptyMap(), Collections.emptyMap()); + } + + /** + * Update the definitions of a single metadata field. + * @param fieldExternalId The id of the field to update + * @param field The field definition + * @return The updated fields definition. + * @throws Exception + */ + public ApiResponse updateMetadataField(String fieldExternalId, MetadataField field) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId); + return callApi(HttpMethod.PUT, uri, ObjectUtils.toMap(field), Collections.singletonMap("content_type", "json")); + } + + /** + * Update the datasource entries for a given field + * @param fieldExternalId The id of the field to update + * @param entries A list of datasource entries. Existing entries (according to entry id) will be updated, + * new entries will be added. + * @return The updated field definition. + * @throws Exception + */ + public ApiResponse updateMetadataFieldDatasource(String fieldExternalId, List entries) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId, "datasource"); + return callApi(HttpMethod.PUT, uri, Collections.singletonMap("values", entries), Collections.singletonMap("content_type", "json")); + } + + /** + * Delete data source entries for a given field + * @param fieldExternalId The id of the field to update + * @param entriesExternalId The ids of all the entries to delete from the data source + * @return The remaining datasource entries. + * @throws Exception + */ + public ApiResponse deleteDatasourceEntries(String fieldExternalId, List entriesExternalId) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId, "datasource"); + return callApi(HttpMethod.DELETE, uri,Collections.singletonMap("external_ids", entriesExternalId) , Collections.emptyMap()); + } + + /** + * Delete a field definition. + * @param fieldExternalId The id of the field to delete + * @return A map with a "message" key. "ok" value indicates a successful deletion. + * @throws Exception + */ + public ApiResponse deleteMetadataField(String fieldExternalId) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId); + return callApi(HttpMethod.DELETE, uri, Collections.emptyMap(), Collections.emptyMap()); + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/Uploader.java b/cloudinary-core/src/main/java/com/cloudinary/Uploader.java index c6e17397..a93cfa0d 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Uploader.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Uploader.java @@ -516,4 +516,23 @@ public String imageUploadTag(String field, Map options, Map html public Map deleteByToken(String token) throws Exception { return callApi("delete_by_token", ObjectUtils.asMap("token", token), ObjectUtils.emptyMap(), null); } + + /** + * Populates metadata fields with the given values. Existing values will be overwritten. + * @param metadata a map of field name and value. + * @param publicIds the public IDs of the resources to update + * @param options additional options passed to the request + * @return a list of public IDs that were updated + * @throws IOException + */ + public Map updateMetadata(Map metadata, String[] publicIds, Map options) throws IOException { + if (options == null) + options = new HashMap(); + + Map params = new HashMap(); + params.put("metadata", Util.encodeContext(metadata)); + params.put("public_ids", Arrays.asList(publicIds)); + + return callApi("metadata", params, options, null); + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/Util.java b/cloudinary-core/src/main/java/com/cloudinary/Util.java index 63f03d94..a6d97df7 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Util.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Util.java @@ -95,6 +95,8 @@ public static final void processWriteParameters(Map options, Map params.put("custom_coordinates", Coordinates.parseCoordinates(options.get("custom_coordinates")).toString()); if (options.get("context") != null) params.put("context", encodeContext(options.get("context"))); + if (options.get("metadata") != null) + params.put("metadata", encodeContext(options.get("metadata"))); if (options.get("access_control") != null) { params.put("access_control", encodeAccessControl(options.get("access_control"))); } diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/DateMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/DateMetadataField.java new file mode 100644 index 00000000..a4df9091 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/DateMetadataField.java @@ -0,0 +1,40 @@ +package com.cloudinary.metadata; + +import com.cloudinary.utils.ObjectUtils; + +import java.text.ParseException; +import java.util.Date; + +/** + * Represents a metadata field with type 'date' + */ +public class DateMetadataField extends MetadataField { + + public DateMetadataField() { + super(MetadataFieldType.DATE); + } + + /** + * Sets the default date used for this field. + * @param defaultValue The date to set. Date only without a time component, UTC assumed. + */ + @Override + public void setDefaultValue(Date defaultValue) { + put(DEFAULT_VALUE, ObjectUtils.toISO8601DateOnly(defaultValue)); + } + + /** + * Get the default value of this date field. + * @return The date only without a time component, UTC. + * @throws ParseException When the underlying value is malformed. + */ + @Override + public Date getDefaultValue() throws ParseException { + Object value = get(DEFAULT_VALUE); + if (value == null) { + return null; + } + + return ObjectUtils.fromISO8601DateOnly(value.toString()); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/EnumMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/EnumMetadataField.java new file mode 100644 index 00000000..79f501c3 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/EnumMetadataField.java @@ -0,0 +1,10 @@ +package com.cloudinary.metadata; + +/** + * Represents a metadata field with 'Enum' type. + */ +public class EnumMetadataField extends MetadataField { + EnumMetadataField() { + super(MetadataFieldType.ENUM); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/IntMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/IntMetadataField.java new file mode 100644 index 00000000..23510210 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/IntMetadataField.java @@ -0,0 +1,10 @@ +package com.cloudinary.metadata; + +/** + * Represents a metadata field with 'Int' type. + */ +public class IntMetadataField extends MetadataField { + public IntMetadataField() { + super(MetadataFieldType.INTEGER); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataDataSource.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataDataSource.java new file mode 100644 index 00000000..043556cd --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataDataSource.java @@ -0,0 +1,70 @@ +package com.cloudinary.metadata; + +import org.cloudinary.json.JSONArray; +import org.cloudinary.json.JSONObject; + +import java.util.List; + +/** + * Represent a data source for a given field. This is used in both 'Set' and 'Enum' field types. + * The datasource holds a list of the valid values to be used with the corresponding metadata field. + */ +public class MetadataDataSource extends JSONObject { + /** + * Creates a new instance of data source with the given list of entries. + * @param entries + */ + public MetadataDataSource(List entries) { + put("values", new JSONArray(entries.toArray())); + } + + /** + * Represents a single entry in a datasource definition for a field. + */ + public static class Entry extends JSONObject { + public Entry(String externalId, String value){ + setExternalId(externalId); + setValue(value); + } + + /** + * Create a new entry with a string value. + * @param value The value to use in the entry. + */ + public Entry(String value){ + this(null, value); + } + + /** + * Set the id of the entry. Will be auto-generated if left blank. + * @param externalId + */ + public void setExternalId(String externalId) { + put("external_id", externalId); + } + + /** + * Get the id of the entry. + * @return + */ + public String getExternalId() { + return optString("external_id"); + } + + /** + * Set the value of the entry. + * @param value The value to set. + */ + public void setValue(String value) { + put("value", value); + } + + /** + * Get the value of the entry. + * @return The value. + */ + public String getValue() { + return optString("value"); + } + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataField.java new file mode 100644 index 00000000..bae2b6c7 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataField.java @@ -0,0 +1,133 @@ +package com.cloudinary.metadata; + +import org.cloudinary.json.JSONObject; + +import java.text.ParseException; + +/** + * Represents a single metadata field. Use one of the derived classes in the metadata API calls. + * @param + */ +public class MetadataField extends JSONObject { + + public static final String DEFAULT_VALUE = "default_value"; + public static final String EXTERNAL_ID = "external_id"; + public static final String LABEL = "label"; + public static final String MANDATORY = "mandatory"; + public static final String TYPE = "type"; + public static final String VALIDATION = "validation"; + + public MetadataField(MetadataFieldType type) { + put(TYPE, type.toString()); + } + + public MetadataField(String type) { + put(TYPE, type); + } + + /** + * The type of the field. + * @return String with the name of the type. + */ + public MetadataFieldType getType() { + return MetadataFieldType.valueOf(optString(TYPE).toUpperCase()); + } + + /** + * Get the id of the field. + * @return String, field id. + */ + public String getExternalId() { + return optString(EXTERNAL_ID); + } + + /** + * Set the id of the string (auto-generated if this is left blank). + * @param externalId The id to set. + */ + public void setExternalId(String externalId) { + put(EXTERNAL_ID, externalId); + } + + /** + * Get the label of the field + * @return String, the label of the field. + */ + public String getLabel() { + return optString(LABEL); + } + + /** + * Sets the label of the field + * @param label The label to set. + */ + public void setLabel(String label) { + put(LABEL, label); + } + + /** + * Cehcks whether the field is mandatory. + * @return Boolean indicating whether the field is mandatory. + */ + public boolean isMandatory() { + return optBoolean(MANDATORY); + } + + /** + * Sets a boolean indicating whether this fields needs to be mandatory. + * @param mandatory The boolean to set. + */ + public void setMandatory(Boolean mandatory) { + put(MANDATORY, mandatory); + } + + /** + * Gets the default value of this field. + * @return The default value + * @throws ParseException If the stored value can't be parsed to the correct type. + */ + public T getDefaultValue() throws ParseException { + //noinspection unchecked + return (T)opt(DEFAULT_VALUE); + } + + /** + * Set the default value of the field + * @param defaultValue The value to set. + */ + public void setDefaultValue(T defaultValue) { + put(DEFAULT_VALUE, defaultValue); + } + + /** + * Get the validation rules of this field. + * @return The validation rules. + */ + public MetadataValidation getValidation() { + return (MetadataValidation) optJSONObject(VALIDATION); + } + + /** + * Set the validation rules of this field. + * @param validation The rules to set. + */ + public void setValidation(MetadataValidation validation) { + put(VALIDATION, validation); + } + + /** + * Get the data source definition of this field. + * @return The data source. + */ + public MetadataDataSource getDataSource() { + return (MetadataDataSource) optJSONObject("datasource"); + } + + /** + * Set the datasource for the field. + * @param dataSource The datasource to set. + */ + public void setDataSource(MetadataDataSource dataSource) { + put("datasource", dataSource); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataFieldType.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataFieldType.java new file mode 100644 index 00000000..34362f27 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataFieldType.java @@ -0,0 +1,17 @@ +package com.cloudinary.metadata; + +/** + * Enum represneting all the valid field types. + */ +public enum MetadataFieldType { + STRING, + INTEGER, + DATE, + ENUM, + SET; + + @Override + public String toString() { + return super.toString().toLowerCase(); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataValidation.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataValidation.java new file mode 100644 index 00000000..f38b732e --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataValidation.java @@ -0,0 +1,177 @@ +package com.cloudinary.metadata; + +import com.cloudinary.utils.ObjectUtils; +import org.cloudinary.json.JSONArray; +import org.cloudinary.json.JSONObject; + +import java.util.Date; +import java.util.List; + +/** + * Represents the base class for metadata fields validation mechanisms. + */ +public abstract class MetadataValidation extends JSONObject { + + public static final String TYPE = "type"; + public static final String MIN = "min"; + public static final String MAX = "max"; + public static final String STRLEN = "strlen"; + public static final String EQUALS = "equals"; + public static final String GREATER_THAN = "greater_than"; + public static final String LESS_THAN = "less_than"; + public static final String VALUE = "value"; + + /** + * An 'And' rule validation used to combine other rules with an 'AND' logic relation between them. + */ + public static class AndValidator extends MetadataValidation { + + public static final String AND = "and"; + + /** + * Create a new instance of the validator with the given rules. + * @param rules The rules to use. + */ + public AndValidator(List rules) { + put(TYPE, AND); + put("rules", new JSONArray(rules.toArray())); + } + } + + /** + * A validator to validate string lengths + */ + public static class StringLength extends MetadataValidation { + /** + * Create a new instance with the given min and max. + * @param min Minimum valid string length. + * @param max Maximum valid string length. + */ + public StringLength(Integer min, Integer max) { + put(TYPE, STRLEN); + put(MIN, min); + put(MAX, max); + } + } + + /** + * Base class for all comparison (greater than/less than) validation rules. + * @param + */ + public abstract static class ComparisonRule extends MetadataValidation { + public ComparisonRule(String type, T value) { + this(type, value, null); + } + + public ComparisonRule(String type, T value, Boolean equals) { + put(TYPE, type); + putValue(value); + if (equals != null) { + put(EQUALS, equals); + } + } + + protected void putValue(T value) { + put(VALUE, value); + } + } + + /** + * Great-than rule for integers. + */ + public static class IntGreaterThan extends ComparisonRule { + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule + */ + public IntGreaterThan(Integer value) { + super(GREATER_THAN, value); + } + + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public IntGreaterThan(Integer value, Boolean equals) { + super(GREATER_THAN, value, equals); + } + } + + /** + * Great-than rule for dates. + */ + public static class DateGreaterThan extends ComparisonRule { + /** + * Create a new rule with the given date. + * @param value The integer to reference in the rule + */ + public DateGreaterThan(Date value) { + super(GREATER_THAN, value); + } + + /** + * Create a new rule with the given date. + * @param value The date to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public DateGreaterThan(Date value, Boolean equals) { + super(GREATER_THAN, value, equals); + } + + @Override + protected void putValue(Date value) { + put(VALUE, ObjectUtils.toISO8601DateOnly(value)); + } + } + + /** + * Less-than rule for integers. + */ + public static class IntLessThan extends ComparisonRule { + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule + */ + public IntLessThan(Integer value) { + super(LESS_THAN, value); + } + + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public IntLessThan(Integer value, Boolean equals) { + super(LESS_THAN, value, equals); + } + } + + /** + * Less-than rule for dates. + */ + public static class DateLessThan extends ComparisonRule { + /** + * Create a new rule with the given date. + * @param value The integer to reference in the rule + */ + public DateLessThan(Date value) { + super(LESS_THAN, value); + } + + /** + * Create a new rule with the given date. + * @param value The date to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public DateLessThan(Date value, Boolean equals) { + super(LESS_THAN, value, equals); + } + + @Override + protected void putValue(Date value) { + put(VALUE, ObjectUtils.toISO8601DateOnly(value)); + } + } +} + diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/SetMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/SetMetadataField.java new file mode 100644 index 00000000..48d54823 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/SetMetadataField.java @@ -0,0 +1,12 @@ +package com.cloudinary.metadata; + +import java.util.List; + +/** + * Represents a metadata field with 'Set' type. + */ +public class SetMetadataField extends MetadataField> { + public SetMetadataField() { + super(MetadataFieldType.SET); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/StringMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/StringMetadataField.java new file mode 100644 index 00000000..e7e03405 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/StringMetadataField.java @@ -0,0 +1,10 @@ +package com.cloudinary.metadata; + +/** + * Represents a metadata field with 'String' type. + */ +public class StringMetadataField extends MetadataField { + public StringMetadataField() { + super(MetadataFieldType.STRING); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractApiStrategy.java b/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractApiStrategy.java index ff358452..e2403372 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractApiStrategy.java +++ b/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractApiStrategy.java @@ -1,11 +1,11 @@ package com.cloudinary.strategies; -import java.util.Map; - import com.cloudinary.Api; import com.cloudinary.Api.HttpMethod; import com.cloudinary.api.ApiResponse; +import java.util.Map; + public abstract class AbstractApiStrategy { protected Api api; diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java b/cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java index 1725ad55..75bbc87c 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java @@ -6,6 +6,7 @@ import java.io.*; import java.text.DateFormat; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; @@ -17,9 +18,19 @@ public class ObjectUtils { * @return The date formatted as ISO-8601 string */ public static String toISO8601(Date date){ + DateFormat dateFormat = getDateFormat(); + return dateFormat.format(date); + } + + public static Date fromISO8601(String date) throws ParseException { + DateFormat dateFormat = getDateFormat(); + return (Date) dateFormat.parseObject(date); + } + + private static DateFormat getDateFormat() { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return dateFormat.format(date); + return dateFormat; } public static String asString(Object value) { @@ -204,4 +215,11 @@ public static Long asLong(Object value, Long defaultValue) { } } + public static String toISO8601DateOnly(Date date) { + return new SimpleDateFormat("yyyy-MM-dd").format(date); + } + + public static Date fromISO8601DateOnly(String string) throws ParseException { + return new SimpleDateFormat("yyyy-MM-dd").parse(string); + } } diff --git a/cloudinary-http42/src/test/java/com/cloudinary/test/StructuredMetadataTest.java b/cloudinary-http42/src/test/java/com/cloudinary/test/StructuredMetadataTest.java new file mode 100644 index 00000000..900da239 --- /dev/null +++ b/cloudinary-http42/src/test/java/com/cloudinary/test/StructuredMetadataTest.java @@ -0,0 +1,4 @@ +package com.cloudinary.test; + +public class StructuredMetadataTest extends AbstractStructuredMetadataTest { +} \ No newline at end of file diff --git a/cloudinary-http43/src/test/java/com/cloudinary/test/StructuredMetadataTest.java b/cloudinary-http43/src/test/java/com/cloudinary/test/StructuredMetadataTest.java new file mode 100644 index 00000000..900da239 --- /dev/null +++ b/cloudinary-http43/src/test/java/com/cloudinary/test/StructuredMetadataTest.java @@ -0,0 +1,4 @@ +package com.cloudinary.test; + +public class StructuredMetadataTest extends AbstractStructuredMetadataTest { +} \ No newline at end of file diff --git a/cloudinary-http44/src/test/java/com/cloudinary/test/StructuredMetadataTest.java b/cloudinary-http44/src/test/java/com/cloudinary/test/StructuredMetadataTest.java new file mode 100644 index 00000000..8cc186f4 --- /dev/null +++ b/cloudinary-http44/src/test/java/com/cloudinary/test/StructuredMetadataTest.java @@ -0,0 +1,4 @@ +package com.cloudinary.test; + +public class StructuredMetadataTest extends AbstractStructuredMetadataTest { +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStructuredMetadataTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStructuredMetadataTest.java new file mode 100644 index 00000000..d370ce5c --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStructuredMetadataTest.java @@ -0,0 +1,252 @@ +package com.cloudinary.test; + +import com.cloudinary.Api; +import com.cloudinary.Cloudinary; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.api.exceptions.BadRequest; +import com.cloudinary.metadata.*; +import org.junit.*; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.util.*; + +import static com.cloudinary.utils.ObjectUtils.asMap; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +public abstract class AbstractStructuredMetadataTest extends MockableTest { + private static final String METADATA_UPLOADER_TAG = SDK_TEST_TAG + "_uploader"; + + protected Api api; + public static final List metadataFieldExternalIds = new ArrayList(); + + @BeforeClass + public static void setUpClass() throws IOException { + Cloudinary cloudinary = new Cloudinary(); + if (cloudinary.config.apiSecret == null) { + System.err.println("Please setup environment for Upload test to run"); + } + } + + @AfterClass + public static void tearDownClass() throws Exception { + Api api = new Cloudinary().api(); + + for (String externalId : metadataFieldExternalIds) { + try { + api.deleteMetadataField(externalId); + } catch (Exception ignored) { + } + } + } + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary(); + assumeNotNull(cloudinary.config.apiSecret); + this.api = cloudinary.api(); + } + + @Test + public void testCreateMetadata() throws Exception { + StringMetadataField stringField = newFieldInstance("testCreateMetadata_1"); + ApiResponse result = addFieldToAccount(stringField); + assertNotNull(result); + assertEquals(stringField.getLabel(), result.get("label")); + + SetMetadataField setField = createSetField("testCreateMetadata_2"); + result = cloudinary.api().addMetadataField(setField); + assertNotNull(result); + assertEquals(setField.getLabel(), result.get("label")); + } + + @Test + public void testDateFieldDefaultValueValidation() throws Exception { + // now minus 3 days hours. + Date max = new Date(); + Date min = new Date(max.getTime() - 72 * 60 * 60 * 1000); + + Date legalValue = new Date(min.getTime() + 36 * 60 * 60 * 1000); + Date illegalValue = new Date(max.getTime() + 36 * 60 * 60 * 1000); + + DateMetadataField dateMetadataField = new DateMetadataField(); + dateMetadataField.setLabel("Start date" + new Date().getTime()); + + List rules = new ArrayList(); + rules.add(new MetadataValidation.DateGreaterThan(min)); + rules.add(new MetadataValidation.DateLessThan(max)); + dateMetadataField.setValidation(new MetadataValidation.AndValidator(rules)); + + String message = null; + ApiResponse res = null; + try { + // should fail + dateMetadataField.setDefaultValue(illegalValue); + res = api.addMetadataField(dateMetadataField); + // this line should not be reached if all is working well, but when it's not we still want to clean it up: + metadataFieldExternalIds.add(res.get("external_id").toString()); + } catch (BadRequest e) { + message = e.getMessage(); + } + + assertEquals(message, "default_value is invalid"); + + // should work: + dateMetadataField.setDefaultValue(legalValue); + res = api.addMetadataField(dateMetadataField); + metadataFieldExternalIds.add(res.get("external_id").toString()); + } + + @Test + public void testListFields() throws Exception { + StringMetadataField stringField = newFieldInstance("testListFields"); + addFieldToAccount(stringField); + + ApiResponse result = cloudinary.api().listMetadataFields(); + assertNotNull(result); + assertNotNull(result.get("metadata_fields")); + assertTrue(((List)result.get("metadata_fields")).size() > 0); + } + + @Test + public void testGetMetadata() throws Exception { + ApiResponse fieldResult = addFieldToAccount(newFieldInstance("testGetMetadata")); + ApiResponse result = api.metadataFieldByFieldId(fieldResult.get("external_id").toString()); + assertNotNull(result); + assertEquals(fieldResult.get("label"), result.get("label")); + } + + @Test + public void testUpdateField() throws Exception { + ApiResponse fieldResult = addFieldToAccount(newFieldInstance("testUpdateField")); + assertNotEquals("new_def", fieldResult.get("default_value")); + StringMetadataField field = new StringMetadataField(); + field.setDefaultValue("new_def"); + ApiResponse result = api.updateMetadataField(fieldResult.get("external_id").toString(), field); + assertNotNull(result); + assertEquals("new_def", result.get("default_value")); + } + + @Test + public void testDeleteField() throws Exception { + ApiResponse fieldResult = addFieldToAccount(newFieldInstance("testDeleteField")); + ApiResponse result = api.deleteMetadataField(fieldResult.get("external_id").toString()); + assertNotNull(result); + assertEquals("ok", result.get("message")); + } + + @Test + public void testUpdateDatasource() throws Exception { + SetMetadataField setField = createSetField("testUpdateDatasource"); + ApiResponse fieldResult = addFieldToAccount(setField); + MetadataDataSource.Entry newEntry = new MetadataDataSource.Entry("id1", "new1"); + ApiResponse result = api.updateMetadataFieldDatasource(fieldResult.get("external_id").toString(), Collections.singletonList(newEntry)); + assertNotNull(result); + assertEquals("new1", ((Map) ((List) result.get("values")).get(0)).get("value")); + } + + @Test + public void testDeleteDatasourceEntries() throws Exception { + SetMetadataField setField = createSetField("testDeleteDatasourceEntries"); + ApiResponse fieldResult = addFieldToAccount(setField); + ApiResponse result = api.deleteDatasourceEntries(fieldResult.get("external_id").toString(), Collections.singletonList("id1")); + assertNotNull(result); + } + + @Test + public void testUploadWithMetadata() throws Exception { + StringMetadataField field = newFieldInstance("testUploadWithMetadata"); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map metadata = Collections.singletonMap(fieldId, "123456"); + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("metadata", metadata, "tags", Arrays.asList(SDK_TEST_TAG, METADATA_UPLOADER_TAG))); + assertNotNull(result.get("metadata")); + assertEquals("123456", ((Map) result.get("metadata")).get(fieldId)); + } + + @Test + public void testExplicitWithMetadata() throws Exception { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, METADATA_UPLOADER_TAG))); + String publicId = uploadResult.get("public_id").toString(); + StringMetadataField field = newFieldInstance("testExplicitWithMetadata"); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map metadata = Collections.singletonMap(fieldId, "123456"); + Map result = cloudinary.uploader().explicit(publicId, asMap("type", "upload", "resource_type", "image", "metadata", metadata)); + assertNotNull(result.get("metadata")); + assertEquals("123456", ((Map) result.get("metadata")).get(fieldId)); + + // explicit with invalid data, should fail: + metadata = Collections.singletonMap(fieldId, "12"); + String message = ""; + try { + result = cloudinary.uploader().explicit(publicId, asMap("type", "upload", "resource_type", "image", "metadata", metadata)); + } catch (Exception e){ + message = e.getMessage(); + } + + assertTrue(message.contains("Value 12 is invalid for field") ); + } + + @Test + public void testUpdateWithMetadata() throws Exception { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, METADATA_UPLOADER_TAG))); + String publicId = uploadResult.get("public_id").toString(); + StringMetadataField field = newFieldInstance("testUpdateWithMetadata"); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map metadata = Collections.singletonMap(fieldId, "123456"); + Map result = cloudinary.api().update(publicId, asMap("type", "upload", "resource_type", "image", "metadata", metadata)); + assertNotNull(result.get("metadata")); + assertEquals("123456", ((Map) result.get("metadata")).get(fieldId)); + } + + @Test + public void testUploaderUpdateMetadata() throws Exception { + StringMetadataField field = newFieldInstance("testUploaderUpdateMetadata"); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map result = cloudinary.uploader().updateMetadata(Collections.singletonMap(fieldId, "123456"), new String[]{"sample"}, null); + assertNotNull(result); + assertEquals("sample", ((List) result.get("public_ids")).get(0).toString()); + } + + // Metadata test helpers + private SetMetadataField createSetField(String labelPrefix) { + SetMetadataField setField = new SetMetadataField(); + String label = labelPrefix + "_" + SUFFIX; + setField.setLabel(label); + setField.setMandatory(false); + setField.setValidation(new MetadataValidation.StringLength(3, 99)); + setField.setDefaultValue(Arrays.asList("id2", "id3")); + setField.setValidation(null); + List entries = new ArrayList(); + entries.add(new MetadataDataSource.Entry("id1", "first_value")); + entries.add(new MetadataDataSource.Entry("id2", "second_value")); + entries.add(new MetadataDataSource.Entry("id3", "third_value")); + MetadataDataSource dataSource = new MetadataDataSource(entries); + setField.setDataSource(dataSource); + return setField; + } + + private StringMetadataField newFieldInstance(String labelPrefix) throws Exception { + StringMetadataField field = new StringMetadataField(); + String label = labelPrefix + "_" + SUFFIX; + field.setLabel(label); + field.setMandatory(true); + field.setValidation(new MetadataValidation.StringLength(3, 9)); + field.setDefaultValue("val_test"); + return field; + } + + private ApiResponse addFieldToAccount(MetadataField field) throws Exception { + ApiResponse apiResponse = api.addMetadataField(field); + metadataFieldExternalIds.add(apiResponse.get("external_id").toString()); + return apiResponse; + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractUploaderTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractUploaderTest.java index c213778e..1208c694 100644 --- a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractUploaderTest.java +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractUploaderTest.java @@ -1,6 +1,8 @@ package com.cloudinary.test; import com.cloudinary.*; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.metadata.StringMetadataField; import com.cloudinary.utils.ObjectUtils; import com.cloudinary.utils.Rectangle; import org.cloudinary.json.JSONArray;