diff --git a/README.md b/README.md index d780e53..55c1b17 100644 --- a/README.md +++ b/README.md @@ -47,16 +47,18 @@ The API supports environment variable `http_proxy`. If the variable is set, it i If `endpointUrl` contains `localhost` or `127.0.0.1` proxy settings are ignored automatically. -## Logging +### Logging -The Simple Logging Facade for Java [https://www.slf4j.org/](SLF4J) serves as a simple facade or abstraction for various logging frameworks (e.g. java.util.logging, logback, log4j) allowing the end user to plug in the desired logging framework at deployment time. +The Simple Logging Facade for Java [https://www.slf4j.org/](SLF4J) serves as a simple facade or abstraction +for various logging frameworks (e.g. java.util.logging, logback, log4j) allowing the end user to plug in the desired +logging framework at deployment time. +### Request Limits +The API of Airtable itself is limited to 5 requests per second. If you exceed this rate, you will receive a 429 status code and will +need to wait 30 seconds before subsequent requests will succeed. -## Access Base -## Access Table - -## CRUD-Operations on table items +## CRUD-Operations on Table Records ## Select Select List of items from table: @@ -87,15 +89,29 @@ Table actorTable = base.table("Actors", Actor.class); Actor actor = actorTable.find("rec514228ed76ced1"); ``` +## Destroy +Use Destroy to delete a specific records of table: + ++ `table(name).destroy(String id)`: delete record with `id` of table `name` + +### Example +```Java +// detailed Example see TableDestroyTest.java +Base base = airtable.base(AIRTABLE_BASE); +Table actorTable = base.table("Actors", Actor.class); +actorTable.destroy("recapJ3Js8AEwt0Bf"); +``` + ## Annotations -Use the Gson Annotation @SerializedName to annotate Names which contain - or an emtpy Charakter. +Use the Gson Annotation `@SerializedName` to annotate Names which contain `-` or emtpy characters. ### Example ```Java import com.google.gson.annotations.SerializedName; + //Column in Airtable is named "First- & Lastname", which is mapped to field "name". @SerializedName("First- & Lastname") private String name; ``` @@ -104,9 +120,9 @@ Use the Gson Annotation @SerializedName to annotate Names which contain - or an + [x] Airtable Configure + [x] configuration of `proxy` + [x] configuration of `AIRTABLE_API_KEY` & `AIRTABLE_BASE` - + [ ] configuration of `requestTimeout` + + [x] configuration of `requestTimeout` -+ [x] Select ++ [x] Select Records + [x] SelectAll + [x] Queries (`maxRecords`, `sort` & `view` ) + [ ] Support of `filterByFormula` @@ -116,7 +132,7 @@ Use the Gson Annotation @SerializedName to annotate Names which contain - or an + [ ] Create Record + [ ] Update Record -+ [ ] Delete Record ++ [x] Delete Record + [ ] Replace Record + General requirements + [ ] Automatic ObjectMapping @@ -151,5 +167,3 @@ We use following libraries: # License MIT License, see [LICENSE](LICENSE) - - diff --git a/src/main/java/com/sybit/airtable/Airtable.java b/src/main/java/com/sybit/airtable/Airtable.java index 4ad73f0..fd0ac23 100644 --- a/src/main/java/com/sybit/airtable/Airtable.java +++ b/src/main/java/com/sybit/airtable/Airtable.java @@ -8,6 +8,8 @@ import com.mashape.unirest.http.Unirest; +import com.sybit.airtable.converter.ListConverter; +import com.sybit.airtable.converter.MapConverter; import com.sybit.airtable.exception.AirtableException; import com.sybit.airtable.vo.Attachment; import com.sybit.airtable.vo.Thumbnail; @@ -42,12 +44,11 @@ public class Airtable { private static final Logger LOG = LoggerFactory.getLogger( Airtable.class ); - private static final String ENDPOINT_URL = "https://api.airtable.com/v0"; + private static final String AIRTABLE_API_KEY = "AIRTABLE_API_KEY"; private static final String AIRTABLE_BASE = "AIRTABLE_BASE"; - private String endpointUrl; - private String apiKey; + private Configuration config; /** * Configure, AIRTABLE_API_KEY passed by Java property, enviroment variable @@ -84,29 +85,32 @@ public Airtable configure() throws AirtableException { */ @SuppressWarnings("WeakerAccess") public Airtable configure(String apiKey) throws AirtableException { - return configure(apiKey, ENDPOINT_URL); + return configure(new Configuration(apiKey, Configuration.ENDPOINT_URL)); } /** * - * @param apiKey - * @param endpointUrl + * @param config * @return * @throws com.sybit.airtable.exception.AirtableException Missing API-Key or Endpoint */ @SuppressWarnings("WeakerAccess") - public Airtable configure(String apiKey, String endpointUrl) throws AirtableException { - if(apiKey == null) { + public Airtable configure(Configuration config) throws AirtableException { + if(config.getApiKey() == null) { throw new AirtableException("Missing Airtable API-Key"); } - if(endpointUrl == null) { + if(config.getEndpointUrl() == null) { throw new AirtableException("Missing endpointUrl"); } - this.apiKey = apiKey; - this.endpointUrl = endpointUrl; + this.config = config; - setProxy(endpointUrl); + if(config.getTimeout() != null) { + LOG.info("Set connection timeout to: " + config.getTimeout() + "ms."); + Unirest.setTimeouts(config.getTimeout(), config.getTimeout()); + } + + setProxy(config.getEndpointUrl()); // Only one time Unirest.setObjectMapper(new GsonObjectMapper()); @@ -188,12 +192,21 @@ public Base base(String base) throws AirtableException { return b; } + public Configuration getConfig() { + return config; + } + + public void setConfig(Configuration config) { + this.config = config; + setProxy(config.getEndpointUrl()); + } + /** * * @return */ public String endpointUrl() { - return endpointUrl; + return this.config.getEndpointUrl(); } /** @@ -201,7 +214,7 @@ public String endpointUrl() { * @return */ public String apiKey() { - return apiKey; + return this.config.getApiKey(); } /** @@ -233,7 +246,7 @@ private String getCredentialProperty(String key) { } public void setEndpointUrl(String endpointUrl) { - this.endpointUrl = endpointUrl; + this.config.setEndpointUrl(endpointUrl); setProxy(endpointUrl); } } diff --git a/src/main/java/com/sybit/airtable/Base.java b/src/main/java/com/sybit/airtable/Base.java index f413ab3..626da80 100644 --- a/src/main/java/com/sybit/airtable/Base.java +++ b/src/main/java/com/sybit/airtable/Base.java @@ -53,7 +53,7 @@ public Base(String base, Airtable airtable) { * Set Airtable object as parent. * @param parent the base Airtable object. */ - protected void setParent(Airtable parent) { + void setParent(Airtable parent) { this.parent = parent; } diff --git a/src/main/java/com/sybit/airtable/Configuration.java b/src/main/java/com/sybit/airtable/Configuration.java new file mode 100644 index 0000000..9a929b0 --- /dev/null +++ b/src/main/java/com/sybit/airtable/Configuration.java @@ -0,0 +1,72 @@ +/* + * The MIT License (MIT) + * Copyright (c) 2017 Sybit GmbH + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + */ +package com.sybit.airtable; + +/** + * Configuration settings for Airtable. + * Used by class Airtable to configure basic settings. + */ +public class Configuration { + + public static final String ENDPOINT_URL = "https://api.airtable.com/v0"; + + private String endpointUrl; + private String apiKey; + private Long timeout; + + /** + * Configure API using given API Key and default endpoint. + * + * @param apiKey + */ + public Configuration(String apiKey) { + this(apiKey, ENDPOINT_URL); + + } + /** + * Configure API using given API Key and default endpointURL. + * + * @param apiKey + * @param endpointUrl + */ + public Configuration(String apiKey, String endpointUrl) { + this.apiKey = apiKey; + this.endpointUrl = endpointUrl; + } + + public String getEndpointUrl() { + return endpointUrl; + } + + public void setEndpointUrl(String endpointUrl) { + this.endpointUrl = endpointUrl; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + /** + * Get connection timeout. + * @return + */ + public Long getTimeout() { + return timeout; + } + + /** + * Set connection timeout. + * @param timeout + */ + public void setTimeout(Long timeout) { + this.timeout = timeout; + } +} diff --git a/src/main/java/com/sybit/airtable/GsonObjectMapper.java b/src/main/java/com/sybit/airtable/GsonObjectMapper.java index 7a21986..1fc5ee0 100644 --- a/src/main/java/com/sybit/airtable/GsonObjectMapper.java +++ b/src/main/java/com/sybit/airtable/GsonObjectMapper.java @@ -15,7 +15,7 @@ * * @author fzr */ -class GsonObjectMapper implements ObjectMapper{ +class GsonObjectMapper implements ObjectMapper { private static final Logger LOG = Logger.getLogger( GsonObjectMapper.class.getName() ); private final Gson gson; @@ -25,11 +25,12 @@ public GsonObjectMapper() { } public T readValue(String value, Class valueType) { - LOG.log(Level.INFO, "readValue: \n" + value); + LOG.log(Level.FINE, "readValue: \n" + value); return gson.fromJson(value, valueType); } public String writeValue(Object value) { + LOG.log(Level.FINE, "writeValue: \n" + value); return gson.toJson(value); } diff --git a/src/main/java/com/sybit/airtable/Query.java b/src/main/java/com/sybit/airtable/Query.java index 16b069c..d22bfb6 100644 --- a/src/main/java/com/sybit/airtable/Query.java +++ b/src/main/java/com/sybit/airtable/Query.java @@ -29,8 +29,10 @@ public interface Query { List getSort(); /** + * Define a filter formula. * - * @return + * see https://support.airtable.com/hc/en-us/articles/203255215-Formula-Field-Reference + * @return get the filter formula. */ String filterByFormula(); } diff --git a/src/main/java/com/sybit/airtable/Table.java b/src/main/java/com/sybit/airtable/Table.java index d1db3cd..9fc47cc 100644 --- a/src/main/java/com/sybit/airtable/Table.java +++ b/src/main/java/com/sybit/airtable/Table.java @@ -13,6 +13,7 @@ import com.mashape.unirest.request.GetRequest; import com.sybit.airtable.exception.AirtableException; import com.sybit.airtable.exception.HttpResponseExceptionHandler; +import com.sybit.airtable.vo.Delete; import com.sybit.airtable.vo.RecordItem; import com.sybit.airtable.vo.Records; import org.apache.commons.beanutils.BeanUtils; @@ -110,7 +111,7 @@ public String filterByFormula() { * @throws HttpResponseException */ @SuppressWarnings("WeakerAccess") - public List select(Query query) throws AirtableException, HttpResponseException { + public List select(Query query) throws AirtableException { HttpResponse response; try { GetRequest request = Unirest.get(getTableEndpointUrl()) @@ -272,7 +273,7 @@ private List getList(HttpResponse response) { * @return searched record. * @throws AirtableException */ - public T find(String id) throws AirtableException, HttpResponseException { + public T find(String id) throws AirtableException { RecordItem body = null; @@ -316,10 +317,38 @@ public T replace(T item) { throw new UnsupportedOperationException("not yet implemented"); } + + /** + * Delete Record by given id + * + * @param id + * @throws AirtableException + */ - public T destroy(T item) { + public void destroy(String id) throws AirtableException { + + Delete body = null; - throw new UnsupportedOperationException("not yet implemented"); + HttpResponse response; + try { + response = Unirest.delete(getTableEndpointUrl() + "/" + id) + .header("accept", "application/json") + .header("Authorization", getBearerToken()) + .asObject(Delete.class); + } catch (UnirestException e) { + throw new AirtableException(e); + } + int code = response.getStatus(); + + if(200 == code) { + body = response.getBody(); + } else { + HttpResponseExceptionHandler.onResponse(response); + } + + if(!body.isDeleted()){ + throw new AirtableException("Record id: "+body.getId()+" could not be deleted."); + } } /** @@ -423,7 +452,7 @@ private void setProperty(T retval, String key, Object value) throws IllegalAcces private String key2property(String key) { if(key.contains(" ") || key.contains("-") ) { - LOG.warn( "Annotate special characters using @SerializedName for property: [" + key + "]"); + LOG.warn( "Annotate columns having special characters by using @SerializedName for property: [" + key + "]"); } String property = key.trim(); property = property.substring(0,1).toLowerCase() + property.substring(1, property.length()); diff --git a/src/main/java/com/sybit/airtable/ListConverter.java b/src/main/java/com/sybit/airtable/converter/ListConverter.java similarity index 98% rename from src/main/java/com/sybit/airtable/ListConverter.java rename to src/main/java/com/sybit/airtable/converter/ListConverter.java index 29b7537..3ce02c3 100644 --- a/src/main/java/com/sybit/airtable/ListConverter.java +++ b/src/main/java/com/sybit/airtable/converter/ListConverter.java @@ -3,14 +3,15 @@ * To change this template file, choose Tools | Templates * and open the template in the editor. */ -package com.sybit.airtable; +package com.sybit.airtable.converter; import com.google.gson.internal.LinkedTreeMap; +import org.apache.commons.beanutils.BeanUtils; +import org.apache.commons.beanutils.converters.AbstractConverter; + import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; -import org.apache.commons.beanutils.BeanUtils; -import org.apache.commons.beanutils.converters.AbstractConverter; /** * @@ -99,7 +100,8 @@ protected T convertToType(final Class type, Object value) throws Instanti * @return A List */ private List toStringList(final Class type, final String value,List returnList) { - + + //FIXME why this if? if (type.equals(String.class)) { returnList.add(String.valueOf(value)); return returnList; diff --git a/src/main/java/com/sybit/airtable/MapConverter.java b/src/main/java/com/sybit/airtable/converter/MapConverter.java similarity index 99% rename from src/main/java/com/sybit/airtable/MapConverter.java rename to src/main/java/com/sybit/airtable/converter/MapConverter.java index 99638c1..390e37d 100644 --- a/src/main/java/com/sybit/airtable/MapConverter.java +++ b/src/main/java/com/sybit/airtable/converter/MapConverter.java @@ -3,7 +3,7 @@ * To change this template file, choose Tools | Templates * and open the template in the editor. */ -package com.sybit.airtable; +package com.sybit.airtable.converter; import com.google.gson.internal.LinkedTreeMap; import com.sybit.airtable.vo.Thumbnail; diff --git a/src/main/java/com/sybit/airtable/vo/Delete.java b/src/main/java/com/sybit/airtable/vo/Delete.java new file mode 100644 index 0000000..9cd0c79 --- /dev/null +++ b/src/main/java/com/sybit/airtable/vo/Delete.java @@ -0,0 +1,47 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.sybit.airtable.vo; + +/** + * + * @author fzr + */ +public class Delete { + + private boolean deleted; + private String id; + + /** + * @return the deleted + */ + public boolean isDeleted() { + return deleted; + } + + /** + * @param deleted the deleted to set + */ + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + /** + * @return the id + */ + public String getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(String id) { + this.id = id; + } + + + +} diff --git a/src/test/java/com/sybit/airtable/TableDestroyTest.java b/src/test/java/com/sybit/airtable/TableDestroyTest.java new file mode 100644 index 0000000..833a8c1 --- /dev/null +++ b/src/test/java/com/sybit/airtable/TableDestroyTest.java @@ -0,0 +1,43 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.sybit.airtable; + +import com.sybit.airtable.exception.AirtableException; +import com.sybit.airtable.movies.Actor; +import com.sybit.airtable.test.WireMockBaseTest; +import java.util.List; +import org.apache.http.client.HttpResponseException; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * + * @author fzr + */ +public class TableDestroyTest extends WireMockBaseTest { + + + + @Test + public void testDestroyMovie() throws AirtableException, HttpResponseException{ + + Base base = airtable.base("appe9941ff07fffcc"); + Table actorTable = base.table("Actors", Actor.class); + + actorTable.destroy("recapJ3Js8AEwt0Bf"); + + } + + @Test (expected = AirtableException.class) + public void testDestroyMovieException() throws AirtableException{ + + Base base = airtable.base("appe9941ff07fffcc"); + Table actorTable = base.table("Actors", Actor.class); + + actorTable.destroy("not succesfull"); + } + +} diff --git a/src/test/java/com/sybit/airtable/movies/Actor.java b/src/test/java/com/sybit/airtable/movies/Actor.java index ec3366b..912edd1 100644 --- a/src/test/java/com/sybit/airtable/movies/Actor.java +++ b/src/test/java/com/sybit/airtable/movies/Actor.java @@ -6,13 +6,19 @@ */ package com.sybit.airtable.movies; -import com.google.gson.annotations.SerializedName; + +import com.sybit.airtable.vo.Attachment; +import java.util.List; public class Actor { private String id; - @SerializedName("Name") private String name; + private List Photo; + private String Biography; + private String[] Filmography; + + public String getId() { return id; @@ -29,4 +35,30 @@ public String getName() { public void setName(String name) { this.name = name; } + + public String[] getFilmography() { + return Filmography; + } + + public void setFilmography(String[] Filmography) { + this.Filmography = Filmography; + } + + public List getPhoto() { + return Photo; + } + + public void setPhoto(List Photo) { + this.Photo = Photo; + } + + public String getBiography() { + return Biography; + } + + public void setBiography(String Biography) { + this.Biography = Biography; + } + + } diff --git a/src/test/resources/__files/body-ActorDelete.json b/src/test/resources/__files/body-ActorDelete.json new file mode 100644 index 0000000..87c5f5a --- /dev/null +++ b/src/test/resources/__files/body-ActorDelete.json @@ -0,0 +1,4 @@ +{ + "deleted": true, + "id": "recapJ3Js8AEwt0Bf" +} \ No newline at end of file diff --git a/src/test/resources/__files/body-Actors.json b/src/test/resources/__files/body-Actors.json index 6c359ff..180e780 100644 --- a/src/test/resources/__files/body-Actors.json +++ b/src/test/resources/__files/body-Actors.json @@ -1,28 +1,62 @@ { - "id": "rec514228ed76ced1", - "fields": { - "Name": "Marlon Brando", - "Filmography": [ - "rec6733da527dd0f1" - ], - "Photo": [ - { - "id": "att1b7c05d697c0c1", - "url": "https://www.filepicker.io/api/file/xlhctgHEQiSGNc0ieMec", - "filename": "220px-Marlon_Brando_-_The_Wild_One.jpg", - "size": 13845, - "type": "image/jpeg", - "thumbnails": { - "small": { - "url": "https://www.filepicker.io/api/file/Rh3L1DL7QAe8nleanQVV" - }, - "large": { - "url": "https://www.filepicker.io/api/file/hcrLCStVRX6LNVEHqtXA" - } + "records": [ + { + "id": "rec514228ed76ced1", + "fields": { + "Name": "Marlon Brando", + "Filmography": [ + "rec6733da527dd0f1" + ], + "Photo": [ + { + "id": "att1b7c05d697c0c1", + "url": "https://www.filepicker.io/api/file/xlhctgHEQiSGNc0ieMec", + "filename": "220px-Marlon_Brando_-_The_Wild_One.jpg", + "size": 13845, + "type": "image/jpeg", + "thumbnails": { + "small": { + "url": "https://www.filepicker.io/api/file/Rh3L1DL7QAe8nleanQVV" + }, + "large": { + "url": "https://www.filepicker.io/api/file/hcrLCStVRX6LNVEHqtXA" + } + } + } + ], + "Biography": "Marlon Brando, Jr. (April 3, 1924 – July 1, 2004) was an American actor. He is hailed for bringing a gripping realism to film acting, and is widely co..." + }, + "createdTime": "2014-07-18T04:48:25.000Z" + }, + { + "id":"recapJ3Js8AEwt0Bf", + "fields": { + "Name":"Test Actor", + "Photo": [ + { + "id":"attfbHPN3hRjTYk3U", + "url":"https://dl.airtable.com/eJd6UGPPTmGPvoTjpP57_Penguins.jpg", + "filename":"Penguins.jpg","size":777835,"type":"image/jpeg", + "thumbnails": { + "small": { + "url":"https://dl.airtable.com/mzOeWfo7RkiX8ueW14gi_small_Penguins.jpg", + "width":48, + "height":36 + }, + "large": { + "url":"https://dl.airtable.com/dMgnXtMrSDKSAQTQqYHa_large_Penguins.jpg", + "width":512, + "height":512 + } + } + } + ], + "Biography":"Test Bio", + "Filmography": [ + "rec6733da527dd0f1" + ] + }, + "createdTime":"2017-03-30T06:47:38.520Z" } - } - ], - "Biography": "Marlon Brando, Jr. (April 3, 1924 – July 1, 2004) was an American actor. He is hailed for bringing a gripping realism to film acting, and is widely co..." - }, - "createdTime": "2014-07-18T04:48:25.000Z" + ] } \ No newline at end of file diff --git a/src/test/resources/mappings/mapping-ActorDelete.json b/src/test/resources/mappings/mapping-ActorDelete.json new file mode 100644 index 0000000..0ce8f0e --- /dev/null +++ b/src/test/resources/mappings/mapping-ActorDelete.json @@ -0,0 +1,10 @@ +{ + "request" : { + "url" : "/v0/appe9941ff07fffcc/Actors/recapJ3Js8AEwt0Bf", + "method" : "DELETE" + }, + "response" : { + "status" : 200, + "bodyFileName" : "/body-ActorDelete.json" + } +} \ No newline at end of file