diff --git a/.gitignore b/.gitignore index 88dc327..ee77796 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ javadoc bin/ lib/ *~ +.svn/ diff --git a/client/src/main/java/br/com/caelum/restfulie/Restfulie.java b/client/src/main/java/br/com/caelum/restfulie/Restfulie.java index fda1427..f896bc4 100644 --- a/client/src/main/java/br/com/caelum/restfulie/Restfulie.java +++ b/client/src/main/java/br/com/caelum/restfulie/Restfulie.java @@ -19,9 +19,11 @@ import java.net.URI; import java.net.URISyntaxException; + import br.com.caelum.restfulie.http.DefaultHttpRequest; import br.com.caelum.restfulie.http.DefaultRestClient; import br.com.caelum.restfulie.http.Request; +import br.com.caelum.restfulie.mediatype.GsonMediaType; /** * Restfulie's client API entry point.
@@ -44,6 +46,13 @@ public static RestClient custom() { return new DefaultRestClient(); } + /** + * Entry point to configure serialization data prior to accessing the resources (json only). + */ + public static RestClient jsonOnly() { + return new DefaultRestClient( new GsonMediaType() ); + } + /** * Entry point to direct access an uri. */ diff --git a/client/src/main/java/br/com/caelum/restfulie/http/DefaultRestClient.java b/client/src/main/java/br/com/caelum/restfulie/http/DefaultRestClient.java index e6db705..be2b84a 100644 --- a/client/src/main/java/br/com/caelum/restfulie/http/DefaultRestClient.java +++ b/client/src/main/java/br/com/caelum/restfulie/http/DefaultRestClient.java @@ -30,6 +30,7 @@ import br.com.caelum.restfulie.http.apache.ApacheDispatcher; import br.com.caelum.restfulie.mediatype.FormEncoded; import br.com.caelum.restfulie.mediatype.JsonMediaType; +import br.com.caelum.restfulie.mediatype.MediaType; import br.com.caelum.restfulie.mediatype.MediaTypes; import br.com.caelum.restfulie.mediatype.XmlMediaType; import br.com.caelum.restfulie.request.RequestDispatcher; @@ -61,6 +62,21 @@ public DefaultRestClient() this.threads = Executors.newCachedThreadPool(); } + /** + * Constructor which only will register the specified media types + * @param medias MediaTypes to be registered with this client + * @author Felipe Brandao + */ + public DefaultRestClient( MediaType...medias ) + { + this.dispatcher = new ApacheDispatcher(this); + this.inflector = new NounPluralizer(); + + for( MediaType media : medias ) this.types.register( media ); + + this.threads = Executors.newCachedThreadPool(); + } + public DefaultRestClient use(RequestDispatcher executor) { this.dispatcher = executor; return this; diff --git a/client/src/main/java/br/com/caelum/restfulie/http/JsonRestClient.java b/client/src/main/java/br/com/caelum/restfulie/http/JsonRestClient.java new file mode 100644 index 0000000..bb47fc8 --- /dev/null +++ b/client/src/main/java/br/com/caelum/restfulie/http/JsonRestClient.java @@ -0,0 +1,108 @@ +package br.com.caelum.restfulie.http; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.jvnet.inflector.Pluralizer; +import org.jvnet.inflector.lang.en.NounPluralizer; + +import br.com.caelum.restfulie.RestClient; +import br.com.caelum.restfulie.RestfulieException; +import br.com.caelum.restfulie.http.apache.ApacheDispatcher; +import br.com.caelum.restfulie.mediatype.GsonMediaType; +import br.com.caelum.restfulie.mediatype.MediaTypes; +import br.com.caelum.restfulie.request.RequestDispatcher; + +/** + * RestClient implementation based on DefaultRestClient + *
+ * This client has been created to allow using restfulie without XML related + * libs, which i consider a bit bloat if users really don't need any XML stuff. + * + * @author Felipe Brandao + * + */ +public class JsonRestClient implements RestClient{ + + private final MediaTypes types = new MediaTypes(); + + private RequestDispatcher dispatcher; + + private Pluralizer inflector; + + private URI lastURI = null; + + private final ExecutorService threads; + + public JsonRestClient(){ + this.dispatcher = new ApacheDispatcher(this); + //TODO don't know if really need to use it + this.inflector = new NounPluralizer(); + types.register(new GsonMediaType()); + this.threads = Executors.newCachedThreadPool(); + } + + public JsonRestClient use(RequestDispatcher executor) { + this.dispatcher = executor; + return this; + } + + public RequestDispatcher getProvider() { + return dispatcher; + } + + public MediaTypes getMediaTypes() { + return types; + } + + /** + * Entry point to direct access an uri. + */ + public Request at(URI uri) { + lastURI = uri; + return createRequestFor(uri); + } + + /** + * Override this method to use your own Request object + * + * @param uri + * @return + */ + protected Request createRequestFor(URI uri) { + return new DefaultHttpRequest(uri, this); + } + + /** + * Entry point to direct access an uri. + * @throws URISyntaxException + */ + public Request at(String uri) { + try { + return at(new URI(uri)); + } catch (URISyntaxException e) { + throw new RestfulieException("Unable to build an URI for this request.", e); + } + } + + public URI lastURI() { + return lastURI; + } + + public Pluralizer inflectionRules() { + return inflector; + } + + public RestClient withInflector(Pluralizer inflector){ + this.inflector = inflector; + return this; + } + + @Override + public ExecutorService getThreads() { + return threads; + } + +} diff --git a/client/src/main/java/br/com/caelum/restfulie/mediatype/GsonMediaType.java b/client/src/main/java/br/com/caelum/restfulie/mediatype/GsonMediaType.java new file mode 100644 index 0000000..701308f --- /dev/null +++ b/client/src/main/java/br/com/caelum/restfulie/mediatype/GsonMediaType.java @@ -0,0 +1,154 @@ +package br.com.caelum.restfulie.mediatype; + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import br.com.caelum.restfulie.RestClient; +import br.com.caelum.restfulie.mediatype.MediaType; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * Media Type implementation for unmarshalling data received in JSON format using GSON library + * + * @author Felipe Brandao + * + */ +public class GsonMediaType implements MediaType{ + + private final static Pattern rootTypeDetectionRegex = Pattern.compile( "^\\{[ ]*\"([^\"]*)\"[ ]*:(.*)}$" ); + + private final List types = Arrays.asList("application/json", "text/json", "json"); + + private Map> classes = new HashMap>(); + private Map collections = new HashMap(); + + private Gson gson; + + public GsonMediaType() { + this.gson = new Gson(); + } + + /** + * Register a class which should be used for unmarshalling. + *
+ * It uses the lower case class name as alias to knows what should be unmarshalled. + *
+ * @see #withType(String, Class) + * @param javaType Java class to be registered + * @return This instance + */ + public GsonMediaType withType( Class javaType ){ + String correctAlias = javaType.getSimpleName(); + correctAlias = correctAlias.substring( 0 , 1 ).toLowerCase() + correctAlias.substring( 1 ); + return this.withType( correctAlias , javaType ); + } + + /** + * Register a class which should be used for unmarshalling. + *
+ * Uses the informed alias to knows what should be unmarshalled. + *
+ * @param alias Alias to the class + * @param javaType Java class to be registered + * @return This instance + */ + public GsonMediaType withType( String alias , Class javaType ){ + this.classes.put( alias , javaType ); + return this; + } + + /** + * Register an alias to identify a JSON resource (usually top level arrays) which should be unmarshalled for a + * specific Collection. + * + * @see TypeToken + * @param alias Collection alias + * @param collectionType Java Type which sould be used (use TypeToken) + * @return This instance + */ + public GsonMediaType withCollection( String alias , Type collectionType ) { + this.collections.put( alias , collectionType ); + return this; + } + + @Override + public boolean answersTo( String type ) { + return types.contains( type ); + } + + @Override + public void marshal( T payload, Writer writer, RestClient client ) throws IOException { + String json = this.gson.toJson( payload ); + System.out.println( "Marshalled object:" + json ); + writer.append( json ); + } + + @Override + public T unmarshal( String content, RestClient client ) { + JsonData jsonData = new JsonData(); + jsonData.json = content; + + Class rootClass = detectRootClass( jsonData ); + if( rootClass != null ){ //we know wich class should be used + return this.gson.fromJson( jsonData.json , rootClass ); + }else{//detection failed, will try to detect as top level array (alias for collection) + Type rootType = detectRootType( jsonData ); + + if( rootType == null ){//we don't know exactly what to do, so let's blow up + throw new IllegalArgumentException( "There's no registered class/collection alias for '" + jsonData.alias + "'" ); + } + + + return this.gson.fromJson( jsonData.json , rootType ); + } + } + + /** + * Detects which class should be used for unmarshalling + * @param + * @param jsonData + * @return A class object for unmarshalling JSON data + */ + @SuppressWarnings("unchecked") + private Class detectRootClass( JsonData jsonData ){ + Matcher matcher = rootTypeDetectionRegex.matcher( jsonData.json ); + if( matcher.matches() ){ + String alias = matcher.group(1); + //defines informations inside jsonData, if fails to detect a registered class for unmarshall (could be a collection) + jsonData.alias = alias; + jsonData.json = matcher.group(2); + + Class javaClass = (Class) this.classes.get( alias ); + return javaClass; + } + return null; + } + + /** + * Detects which type should be used for unmarshalling a top level array + * @param jsonData + * @return + */ + private Type detectRootType( JsonData jsonData ){ + return this.collections.get( jsonData.alias ); + } + + + /** + * Handler for value related with the (un)marshalling + * @author felipebn + */ + private final static class JsonData{ + String alias; + String json; + } +} diff --git a/client/src/test/java/br/com/caelum/restfulie/GsonMediaTypeTest.java b/client/src/test/java/br/com/caelum/restfulie/GsonMediaTypeTest.java new file mode 100644 index 0000000..3a4b231 --- /dev/null +++ b/client/src/test/java/br/com/caelum/restfulie/GsonMediaTypeTest.java @@ -0,0 +1,82 @@ +package br.com.caelum.restfulie; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; +import org.jvnet.inflector.lang.en.NounPluralizer; + +import br.com.caelum.restfulie.mediatype.GsonMediaType; + +import com.google.gson.reflect.TypeToken; + +public class GsonMediaTypeTest { + + public static class Order{ + private int id; + private String client; + public Order() {} + Order(int id, String client) { + this.id = id; + this.client = client; + } + @Override + public boolean equals( Object obj ) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Order other = (Order) obj; + if (client == null) { + if (other.client != null) + return false; + } else if (!client.equals( other.client )) + return false; + if (id != other.id) + return false; + return true; + } + } + + private GsonMediaType mediaType; + private RestClient client = mock(RestClient.class); + + @Before + public void setup() { + Locale.setDefault(Locale.ENGLISH); + mediaType = new GsonMediaType() + .withType(Order.class) + .withCollection( "orders", new TypeToken>(){}.getType() ); + when(client.inflectionRules()).thenReturn(new NounPluralizer()); + } + + @Test + public void shouldDeserializeWithoutLinks() { + String json = "{\"order\":{\"id\":10,\"client\":\"Jonh Doe\"}}"; + Order expected = new Order(10,"Jonh Doe"); + Order order = mediaType.unmarshal(json, client); + assertThat(order, is(equalTo(expected))); + } + + @Test + public void shouldDeserializeCollections() { + String json = "{\"orders\":[{\"id\":10,\"client\":\"Jonh Doe\"},{\"id\":20,\"client\":\"Jane Doe\"}]}"; + Order expected = new Order(10,"Jonh Doe"); + Order expected2 = new Order(20,"Jane Doe"); + List orders = mediaType.unmarshal(json, client); + assertThat(orders, hasItem(equalTo(expected))); + assertThat(orders, hasItem(equalTo(expected2))); + } + +}