diff --git a/build/tmpl/text/changes.txt b/build/tmpl/text/changes.txt index 21a47c308f..3a871909d1 100644 --- a/build/tmpl/text/changes.txt +++ b/build/tmpl/text/changes.txt @@ -18,6 +18,14 @@ Changes log - Bugs fixed - @Status now works with annotated methods on the server-side. + + - API changes + - Deprecated statusService property in Component to keep it only in Application. + - Added connegService, metadataService and converterService properties to StatusService + and pass them via the constructor instead of the toRepresentation and toStatus methods. + - Renamed @Status#serializeProperties (default to false) into serialize (default to true). + - Deprecated Component#statusService in favor of the Application#statusService property. + - 2.3 Milestone 3 (09/18/2014) - New features @@ -36,7 +44,7 @@ Changes log methods accepting 'null' string values to facilitate the processing of path variables and query parameters. - Renamed StatusService methods from getRepresentation(...) and getStatus(...) to - toRepresentation(...) and toStatus(...), added additional parameters to allow + toRepresentation(...) and toStatus(...), added throwable parameters to allow access to throwable and converter service if available. - Aligned behavior of StatusService (underlying StatusFilter) with the behavior of the ServerResource#doCatch() method in case a throwable is intercepted. diff --git a/modules/org.restlet.ext.servlet/src/org/restlet/ext/servlet/ServerServlet.java b/modules/org.restlet.ext.servlet/src/org/restlet/ext/servlet/ServerServlet.java index 4cf89b882f..788a1926b4 100644 --- a/modules/org.restlet.ext.servlet/src/org/restlet/ext/servlet/ServerServlet.java +++ b/modules/org.restlet.ext.servlet/src/org/restlet/ext/servlet/ServerServlet.java @@ -423,6 +423,7 @@ protected ServerCall createCall(Server server, HttpServletRequest request, * * @return The newly created Component or null if unable to create. */ + @SuppressWarnings("deprecation") protected Component createComponent() { // Detect both customized Component and configuration with restlet.xml // file. diff --git a/modules/org.restlet.test/src/org/restlet/test/ext/sip/AddressTestCase.java b/modules/org.restlet.test/src/org/restlet/test/ext/sip/AddressTestCase.java index 6a916bc3b1..a235c1f11c 100644 --- a/modules/org.restlet.test/src/org/restlet/test/ext/sip/AddressTestCase.java +++ b/modules/org.restlet.test/src/org/restlet/test/ext/sip/AddressTestCase.java @@ -33,6 +33,8 @@ package org.restlet.test.ext.sip; +import java.io.IOException; + import org.junit.Test; import org.restlet.data.Parameter; import org.restlet.data.Reference; @@ -97,7 +99,7 @@ public void testParsing() throws Exception { } @Test - public void testWriting() { + public void testWriting() throws IOException { Address a = new Address(); a.setDisplayName("A. G. Bell"); a.setReference(new Reference("sip:agb@bell-telephone.com")); @@ -114,6 +116,7 @@ public void testWriting() { a.getParameters().add("tag", "a48s"); assertEquals(" ;tag=a48s", w.append(a) .toString()); + w.close(); } } diff --git a/modules/org.restlet.test/src/org/restlet/test/resource/AnnotatedResource20TestCase.java b/modules/org.restlet.test/src/org/restlet/test/resource/AnnotatedResource20TestCase.java index 93c4c16088..0a1ed0ec9b 100644 --- a/modules/org.restlet.test/src/org/restlet/test/resource/AnnotatedResource20TestCase.java +++ b/modules/org.restlet.test/src/org/restlet/test/resource/AnnotatedResource20TestCase.java @@ -34,11 +34,16 @@ package org.restlet.test.resource; import java.io.IOException; +import java.util.Map; +import org.restlet.Application; +import org.restlet.data.MediaType; import org.restlet.engine.Engine; import org.restlet.ext.jackson.JacksonConverter; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.representation.StatusInfo; import org.restlet.resource.ClientResource; -import org.restlet.resource.Finder; import org.restlet.resource.ResourceException; import org.restlet.test.RestletTestCase; @@ -59,11 +64,15 @@ protected void setUp() throws Exception { Engine.getInstance().getRegisteredConverters() .add(new JacksonConverter()); Engine.getInstance().registerDefaultConverters(); - Finder finder = new Finder(); - finder.setTargetClass(MyServerResource20.class); + + // Hosts resources into an Application because we need some services for + // handling content negotiation, conversion of exceptions, etc. + Application application = new Application(); + application.setInboundRoot(MyServerResource20.class); this.clientResource = new ClientResource("http://local"); - this.clientResource.setNext(finder); + this.clientResource.accept(MediaType.APPLICATION_JSON); + this.clientResource.setNext(application); this.myResource = clientResource.wrap(MyResource20.class); } @@ -77,9 +86,42 @@ protected void tearDown() throws Exception { public void testGet() throws IOException, ResourceException { try { myResource.represent(); - } catch (MyException e) { - assertNotNull(e.getDate()); + fail("Should fail"); + } catch (MyException01 e) { + fail("Exception should be caught by client resource"); + } catch (ResourceException e) { + assertEquals(400, e.getStatus().getCode()); + Representation responseEntity = clientResource.getResponseEntity(); + if (responseEntity instanceof JacksonRepresentation) { + assertTrue(JacksonRepresentation.class + .isAssignableFrom(responseEntity.getClass())); + JacksonRepresentation jacksonRepresentation = (JacksonRepresentation) responseEntity; + Object entity = jacksonRepresentation.getObject(); + assertTrue(StatusInfo.class.isAssignableFrom(entity + .getClass())); + StatusInfo statusInfo = (StatusInfo) entity; + assertEquals(400, statusInfo.getCode()); + } } } + public void testGetAndSerializeException() throws IOException, + ResourceException { + try { + myResource.representAndSerializeException(); + fail("Should fail"); + } catch (MyException02 e) { + fail("Exception should be caught by client resource"); + } catch (ResourceException e) { + assertEquals(400, e.getStatus().getCode()); + Representation responseEntity = clientResource.getResponseEntity(); + assertTrue(JacksonRepresentation.class + .isAssignableFrom(responseEntity.getClass())); + JacksonRepresentation jacksonRepresentation = (JacksonRepresentation) responseEntity; + Object entity = jacksonRepresentation.getObject(); + assertTrue(Map.class.isAssignableFrom(entity.getClass())); + Map map = (Map) entity; + assertEquals("my custom error", map.get("customProperty")); + } + } } diff --git a/modules/org.restlet.test/src/org/restlet/test/resource/MyException.java b/modules/org.restlet.test/src/org/restlet/test/resource/MyException01.java similarity index 72% rename from modules/org.restlet.test/src/org/restlet/test/resource/MyException.java rename to modules/org.restlet.test/src/org/restlet/test/resource/MyException01.java index f37ef9617d..fd0ae2665f 100644 --- a/modules/org.restlet.test/src/org/restlet/test/resource/MyException.java +++ b/modules/org.restlet.test/src/org/restlet/test/resource/MyException01.java @@ -4,14 +4,14 @@ import org.restlet.resource.Status; -@Status("401") -public class MyException extends Throwable { +@Status(value = 400, serialize = false) +public class MyException01 extends Throwable { private static final long serialVersionUID = 1L; private Date date; - public MyException(Date date) { + public MyException01(Date date) { this.date = date; } diff --git a/modules/org.restlet.test/src/org/restlet/test/resource/MyException02.java b/modules/org.restlet.test/src/org/restlet/test/resource/MyException02.java new file mode 100644 index 0000000000..a42984f352 --- /dev/null +++ b/modules/org.restlet.test/src/org/restlet/test/resource/MyException02.java @@ -0,0 +1,23 @@ +package org.restlet.test.resource; + +import org.restlet.resource.Status; + +@Status(value = 400) +public class MyException02 extends Throwable { + + private static final long serialVersionUID = 1L; + + private String customProperty; + + public MyException02(String customProperty) { + this.customProperty = customProperty ; + } + + public String getCustomProperty() { + return customProperty; + } + + public void setCustomProperty(String customProperty) { + this.customProperty = customProperty; + } +} diff --git a/modules/org.restlet.test/src/org/restlet/test/resource/MyResource20.java b/modules/org.restlet.test/src/org/restlet/test/resource/MyResource20.java index b92e7b81ad..dee35b114a 100644 --- a/modules/org.restlet.test/src/org/restlet/test/resource/MyResource20.java +++ b/modules/org.restlet.test/src/org/restlet/test/resource/MyResource20.java @@ -34,6 +34,7 @@ package org.restlet.test.resource; import org.restlet.resource.Get; +import org.restlet.resource.Put; /** * Sample annotated interface. @@ -43,6 +44,9 @@ public interface MyResource20 { @Get - MyBean represent() throws MyException; + MyBean represent() throws MyException01; + + @Put + MyBean representAndSerializeException() throws MyException02; } diff --git a/modules/org.restlet.test/src/org/restlet/test/resource/MyServerResource20.java b/modules/org.restlet.test/src/org/restlet/test/resource/MyServerResource20.java index aaa54ebde5..2605f19b39 100644 --- a/modules/org.restlet.test/src/org/restlet/test/resource/MyServerResource20.java +++ b/modules/org.restlet.test/src/org/restlet/test/resource/MyServerResource20.java @@ -52,15 +52,13 @@ public static void main(String[] args) throws Exception { server.start(); } - private volatile MyBean myBean = new MyBean("myName", "myDescription"); - - @SuppressWarnings("unused") - public MyBean represent() throws MyException { - if (true) { - throw new MyException(new Date()); - } + public MyBean represent() throws MyException01 { + throw new MyException01(new Date()); + } - return myBean; + @Override + public MyBean representAndSerializeException() throws MyException02 { + throw new MyException02("my custom error"); } } diff --git a/modules/org.restlet.test/src/org/restlet/test/service/StatusServiceTestCase.java b/modules/org.restlet.test/src/org/restlet/test/service/StatusServiceTestCase.java index 2cd542545e..454fbb2b2e 100644 --- a/modules/org.restlet.test/src/org/restlet/test/service/StatusServiceTestCase.java +++ b/modules/org.restlet.test/src/org/restlet/test/service/StatusServiceTestCase.java @@ -33,12 +33,24 @@ package org.restlet.test.service; +import java.io.IOException; import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.data.MediaType; import org.restlet.data.Status; +import org.restlet.ext.jackson.JacksonRepresentation; +import org.restlet.representation.Representation; +import org.restlet.service.ConnegService; +import org.restlet.service.ConverterService; +import org.restlet.service.MetadataService; import org.restlet.service.StatusService; import org.restlet.test.RestletTestCase; -import org.restlet.test.resource.MyException; +import org.restlet.test.resource.MyException01; +import org.restlet.test.resource.MyException02; /** * Unit tests for the status service. @@ -49,8 +61,62 @@ public class StatusServiceTestCase extends RestletTestCase { public void testAnnotation() { StatusService ss = new StatusService(); - Status status = ss.toStatus(new MyException(new Date()), null, null); - assertEquals(401, status.getCode()); + Status status = ss.toStatus(new MyException01(new Date()), null, null); + assertEquals(400, status.getCode()); + } + + public void testRepresentation() throws IOException { + Status status = new Status(400, new MyException01(new Date())); + + ConverterService converterService = new ConverterService(); + ConnegService connegService = new ConnegService(); + MetadataService metadataService = new MetadataService(); + StatusService ss = new StatusService(true, converterService, + metadataService, connegService); + + Request request = new Request(); + Representation representation = ss.toRepresentation(status, + request, new Response(request)); + + // verify + Status expectedStatus = Status.CLIENT_ERROR_BAD_REQUEST; + HashMap expectedRepresentationMap = new LinkedHashMap(); + expectedRepresentationMap.put("code", expectedStatus.getCode()); + expectedRepresentationMap.put("reasonPhrase", + expectedStatus.getReasonPhrase()); + expectedRepresentationMap.put("description", + expectedStatus.getDescription()); + String expectedJsonRepresentation = new JacksonRepresentation>( + expectedRepresentationMap).getText(); + + Status.CLIENT_ERROR_BAD_REQUEST.getCode(); + assertEquals(MediaType.APPLICATION_JSON, representation.getMediaType()); + assertEquals(expectedJsonRepresentation, representation.getText()); + } + + public void testSerializedException() + throws IOException { + Status status = new Status(400, new MyException02("test message")); + + ConverterService converterService = new ConverterService(); + ConnegService connegService = new ConnegService(); + MetadataService metadataService = new MetadataService(); + StatusService ss = new StatusService(true, converterService, + metadataService, connegService); + + Request request = new Request(); + Representation representation = ss.toRepresentation(status, + request, new Response(request)); + + // verify + HashMap expectedRepresentationMap = new LinkedHashMap(); + expectedRepresentationMap.put("customProperty", "test message"); + String expectedJsonRepresentation = new JacksonRepresentation>( + expectedRepresentationMap).getText(); + + Status.CLIENT_ERROR_BAD_REQUEST.getCode(); + assertEquals(MediaType.APPLICATION_JSON, representation.getMediaType()); + assertEquals(expectedJsonRepresentation, representation.getText()); } } diff --git a/modules/org.restlet/module.xml b/modules/org.restlet/module.xml index 9fa01441da..f2909c1170 100644 --- a/modules/org.restlet/module.xml +++ b/modules/org.restlet/module.xml @@ -84,6 +84,7 @@ + @@ -118,6 +119,7 @@ + @@ -134,6 +136,7 @@ + diff --git a/modules/org.restlet/src/org/restlet/Application.java b/modules/org.restlet/src/org/restlet/Application.java index 1ce4221a5a..4a16757d24 100644 --- a/modules/org.restlet/src/org/restlet/Application.java +++ b/modules/org.restlet/src/org/restlet/Application.java @@ -157,19 +157,24 @@ public Application(Context context) { this.helper.setContext(context); } + ConnegService connegService = new ConnegService(); + ConverterService converterService = new ConverterService(); + MetadataService metadataService = new MetadataService(); + this.outboundRoot = null; this.inboundRoot = null; this.roles = new CopyOnWriteArrayList(); this.services = new ServiceList(context); this.services.add(new TunnelService(true, true)); - this.services.add(new StatusService()); + this.services.add(new StatusService(true, converterService, + metadataService, connegService)); this.services.add(new DecoderService()); this.services.add(new EncoderService(false)); this.services.add(new RangeService()); this.services.add(new ConnectorService()); - this.services.add(new ConnegService()); - this.services.add(new ConverterService()); - this.services.add(new MetadataService()); + this.services.add(connegService); + this.services.add(converterService); + this.services.add(metadataService); // [ifndef gae] this.services.add(new org.restlet.service.TaskService(false)); diff --git a/modules/org.restlet/src/org/restlet/Component.java b/modules/org.restlet/src/org/restlet/Component.java index ae86a7242c..2073607aa7 100644 --- a/modules/org.restlet/src/org/restlet/Component.java +++ b/modules/org.restlet/src/org/restlet/Component.java @@ -384,7 +384,9 @@ public ServiceList getServices() { * Returns the status service, enabled by default. * * @return The status service. + * @deprecated Use {@link Application#getStatusService()} instead. */ + @Deprecated public StatusService getStatusService() { return getServices().get(StatusService.class); } @@ -530,7 +532,10 @@ public void setServers(ServerList servers) { * * @param statusService * The status service. + * @deprecated Use {@link Application#setStatusService(StatusService)} + * instead. */ + @Deprecated public void setStatusService(StatusService statusService) { getServices().set(statusService); } diff --git a/modules/org.restlet/src/org/restlet/data/Status.java b/modules/org.restlet/src/org/restlet/data/Status.java index 5576149fb3..e7becd7d8a 100644 --- a/modules/org.restlet/src/org/restlet/data/Status.java +++ b/modules/org.restlet/src/org/restlet/data/Status.java @@ -35,7 +35,6 @@ import org.restlet.engine.Edition; import org.restlet.engine.Engine; -import org.restlet.service.StatusService; /** * Status to return after handling a call. @@ -1092,7 +1091,7 @@ public int getCode() { /** * Returns the description. This value is typically used by the - * {@link StatusService} to build a meaningful description of an error via a + * {@link org.restlet.service.StatusService} to build a meaningful description of an error via a * response entity. * * @return The description. diff --git a/modules/org.restlet/src/org/restlet/engine/application/StatusFilter.java b/modules/org.restlet/src/org/restlet/engine/application/StatusFilter.java index 334ccf820a..e06b9e455d 100644 --- a/modules/org.restlet/src/org/restlet/engine/application/StatusFilter.java +++ b/modules/org.restlet/src/org/restlet/engine/application/StatusFilter.java @@ -45,7 +45,6 @@ import org.restlet.representation.Representation; import org.restlet.representation.StringRepresentation; import org.restlet.routing.Filter; -import org.restlet.service.ConverterService; import org.restlet.service.StatusService; // [excludes gwt] @@ -115,8 +114,14 @@ public StatusFilter(Context context, StatusService statusService) { } /** - * Allows filtering after its handling by the target Restlet. Does nothing + * Allows filtering after its handling by the target Restlet. + * If the status is not set, set {@link org.restlet.data.Status#SUCCESS_OK} * by default. + * + * If this is an error status, try to get a representation of it with + * {@link org.restlet.service.StatusService#toStatus(Throwable, org.restlet.Request, org.restlet.Response)}. + * If the representation is null, get a default one with + * {@link #getDefaultRepresentation(org.restlet.data.Status, org.restlet.Request, org.restlet.Response)}. * * @param request * The request to handle. @@ -133,15 +138,28 @@ public void afterHandle(Request request, Response response) { // Do we need to get a representation for the current status? if (response.getStatus().isError() && ((response.getEntity() == null) || isOverwriting())) { - response.setEntity(toRepresentation(response.getStatus(), null, - request, response)); + + Representation representation = null; + + try { + representation = getStatusService().toRepresentation(response.getStatus(), + request, response); + } catch (Exception e) { + getLogger().log(Level.WARNING, + "Unable to get the custom status representation", e); + } + + if (representation == null) { + representation = getDefaultRepresentation(response.getStatus(), request, response); + } + response.setEntity(representation); } } /** * Handles the call by distributing it to the next Restlet. If a throwable - * is caught, the {@link #toStatus(Throwable, Request, Response)} method is - * invoked. + * is caught, the {@link org.restlet.service.StatusService#toStatus(Throwable, Request, Response)} + * method is invoked. * * @param request * The request to handle. @@ -155,10 +173,10 @@ protected int doHandle(Request request, Response response) { try { super.doHandle(request, response); } catch (Throwable throwable) { - Level level = Level.INFO; Status status = getStatusService().toStatus(throwable, request, response); + Level level = Level.INFO; if (status.isServerError()) { level = Level.WARNING; } else if (status.isConnectorError()) { @@ -166,18 +184,11 @@ protected int doHandle(Request request, Response response) { } else if (status.isClientError()) { level = Level.FINE; } - getLogger().log(level, "Exception or error caught by status service", throwable); if (response != null) { response.setStatus(status); - - if ((response.getEntity() == null) || isOverwriting()) { - response.setEntity(getStatusService().toRepresentation( - status, throwable, request, response, - getApplication().getConverterService())); - } } } @@ -194,16 +205,6 @@ public String getContactEmail() { return contactEmail; } - /** - * Returns the converter service of the application if available or null. - * - * @return The converter service of the application if available or null. - */ - public ConverterService getConverterService() { - return (getApplication() == null) ? null : getApplication() - .getConverterService(); - } - /** * Returns a representation for the given status.
* In order to customize the default representation, this method can be @@ -267,49 +268,6 @@ public Reference getHomeRef() { return homeRef; } - /** - * Returns a representation for the given status.
- * In order to customize the default representation, this method can be - * overridden. - * - * @param status - * The status to represent. - * @param request - * The request handled. - * @param response - * The response updated. - * @return The representation of the given status. - * @deprecated Use {@link #toRepresentation(Status, Request, Response)} - * instead. - */ - @Deprecated - protected Representation getRepresentation(Status status, Request request, - Response response) { - return toRepresentation(status, null, request, response); - } - - /** - * Returns a status for a given exception or error. By default it returns an - * {@link Status#SERVER_ERROR_INTERNAL} status including the related error - * or exception and logs a severe message.
- * In order to customize the default behavior, this method can be - * overridden. - * - * @param throwable - * The exception or error caught. - * @param request - * The request handled. - * @param response - * The response updated. - * @return The representation of the given status. - * @deprecated Use {@link #toStatus(Throwable, Request, Response)} instead. - */ - @Deprecated - protected Status getStatus(Throwable throwable, Request request, - Response response) { - return toStatus(throwable, request, response); - } - /** * Returns the status information to display in the default representation. * By default it returns the status's reason phrase. @@ -382,56 +340,4 @@ public void setStatusService(StatusService statusService) { this.statusService = statusService; } - /** - * Returns a representation for the given status.
- * In order to customize the default representation, this method can be - * overridden. - * - * @param status - * The status to represent. - * @param throwable - * The exception or error caught. - * @param request - * The request handled. - * @param response - * The response updated. - * @return The representation of the given status. - */ - protected Representation toRepresentation(Status status, - Throwable throwable, Request request, Response response) { - Representation result = null; - - try { - result = getStatusService().toRepresentation(status, throwable, - request, response, getConverterService()); - } catch (Exception e) { - getLogger().log(Level.WARNING, - "Unable to get the custom status representation", e); - } - - if (result == null) { - result = getDefaultRepresentation(status, request, response); - } - - return result; - } - - /** - * Returns a status for a given exception or error. By default it returns an - * {@link Status#SERVER_ERROR_INTERNAL} status including the related error - * or exception and logs a severe message.
- * In order to customize the default behavior, this method can be overriden. - * - * @param throwable - * The exception or error caught. - * @param request - * The request handled. - * @param response - * The response updated. - * @return The representation of the given status. - */ - protected Status toStatus(Throwable throwable, Request request, - Response response) { - return getStatusService().toStatus(throwable, request, response); - } } diff --git a/modules/org.restlet/src/org/restlet/engine/component/ComponentXmlParser.java b/modules/org.restlet/src/org/restlet/engine/component/ComponentXmlParser.java index 1a39d4f7a5..36dc384aaa 100644 --- a/modules/org.restlet/src/org/restlet/engine/component/ComponentXmlParser.java +++ b/modules/org.restlet/src/org/restlet/engine/component/ComponentXmlParser.java @@ -440,6 +440,7 @@ private Representation getXmlConfiguration() { /** * Parse a configuration file and update the component's configuration. */ + @SuppressWarnings("deprecation") public void parse() { try { // Parse and validate the XML configuration diff --git a/modules/org.restlet/src/org/restlet/engine/resource/AnnotationUtils.java b/modules/org.restlet/src/org/restlet/engine/resource/AnnotationUtils.java index cf5c58db59..c4ca0a38e6 100644 --- a/modules/org.restlet/src/org/restlet/engine/resource/AnnotationUtils.java +++ b/modules/org.restlet/src/org/restlet/engine/resource/AnnotationUtils.java @@ -27,7 +27,7 @@ * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework - * + * * Restlet is a registered trademark of Restlet S.A.S. */ @@ -44,6 +44,7 @@ import org.restlet.data.Method; import org.restlet.representation.Representation; import org.restlet.resource.ServerResource; +import org.restlet.resource.Status; import org.restlet.service.MetadataService; // [excludes gwt] @@ -147,8 +148,9 @@ private List addStatusAnnotationDescriptors( .getAnnotation(org.restlet.resource.Status.class); if (annotation != null) { - result.add(new StatusAnnotationInfo(initialClass, - ((org.restlet.resource.Status) annotation).value())); + Status status = (Status) annotation; + result.add(new StatusAnnotationInfo(initialClass, status.value(), + status.serialize())); } return result; diff --git a/modules/org.restlet/src/org/restlet/engine/resource/StatusAnnotationInfo.java b/modules/org.restlet/src/org/restlet/engine/resource/StatusAnnotationInfo.java index 5355c9cf88..b8a4f35828 100644 --- a/modules/org.restlet/src/org/restlet/engine/resource/StatusAnnotationInfo.java +++ b/modules/org.restlet/src/org/restlet/engine/resource/StatusAnnotationInfo.java @@ -46,24 +46,25 @@ public class StatusAnnotationInfo extends AnnotationInfo { /** The status parsed from the annotation value. */ private final Status status; + /** Indicates if the {@link Throwable} should be serialized. */ + private final boolean serialize; + /** * Constructor. * * @param javaClass * The class or interface that hosts the annotated Java method. - * @param value - * The annotation value. + * @param code + * The status code + * @param serialize + * Indicates if the {@link Throwable} should be serialized. */ - public StatusAnnotationInfo(Class javaClass, String value) { - super(javaClass, null, value); + public StatusAnnotationInfo(Class javaClass, int code, boolean serialize) { + super(javaClass, null, Integer.toString(code)); // Parse the main components of the annotation value - if ((value != null) && !value.equals("")) { - Integer code = Integer.parseInt(value); - this.status = Status.valueOf(code); - } else { - this.status = Status.SERVER_ERROR_INTERNAL; - } + this.status = Status.valueOf(code); + this.serialize = serialize; } /** @@ -101,11 +102,22 @@ public Status getStatus() { return status; } + /** + * Returns the serializeProperties indicator parsed from the annotation + * value. + * + * @return the serializeProperties indicator parsed from the annotation + * value. + */ + public boolean isSerialize() { + return serialize; + } + @Override public String toString() { return "StatusAnnotationInfo [javaMethod: " + javaMethod + ", javaClass: " + getJavaClass() + ", status: " + status - + "]"; + + ", serializeProperties: " + serialize + "]"; } } diff --git a/modules/org.restlet/src/org/restlet/engine/util/BeanInfoUtils.java b/modules/org.restlet/src/org/restlet/engine/util/BeanInfoUtils.java new file mode 100644 index 0000000000..716bdb5333 --- /dev/null +++ b/modules/org.restlet/src/org/restlet/engine/util/BeanInfoUtils.java @@ -0,0 +1,52 @@ +package org.restlet.engine.util; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Utilities to get the {@link BeanInfo} of a class. + * + * @author Manuel Boillod + */ +public class BeanInfoUtils { + + /** BeanInfo cache. */ + private static final ConcurrentMap, BeanInfo> cache = new ConcurrentHashMap, BeanInfo>(); + + /** + * Get a BeanInfo from cache or create it. + * Stop introspection to {@link Object} or {@link Throwable} if the class + * is a subtype of {@link Throwable} + * + * @param clazz + * The class + * @return BeanInfo of the class + */ + public static BeanInfo getBeanInfo(Class clazz) { + BeanInfo result = cache.get(clazz); + + if (result == null) { + // Inspect the class itself for annotations + + Class stopClass = Throwable.class.isAssignableFrom(clazz) ? Throwable.class : Object.class; + try { + result = Introspector.getBeanInfo(clazz, stopClass, Introspector.IGNORE_ALL_BEANINFO); + } catch (IntrospectionException e) { + throw new RuntimeException("Could not get BeanInfo of class " + clazz.getName(), e); + } + + // Put the list in the cache if no one was previously present + BeanInfo prev = cache.putIfAbsent(clazz, result); + + if (prev != null) { + // Reuse the previous entry + result = prev; + } + } + + return result; + } +} diff --git a/modules/org.restlet/src/org/restlet/engine/util/ThrowableSerializer.java b/modules/org.restlet/src/org/restlet/engine/util/ThrowableSerializer.java new file mode 100644 index 0000000000..dfd1f425e0 --- /dev/null +++ b/modules/org.restlet/src/org/restlet/engine/util/ThrowableSerializer.java @@ -0,0 +1,74 @@ +/** + * Copyright 2005-2014 Restlet + * + * The contents of this file are subject to the terms of one of the following + * open source licenses: Apache 2.0 or LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL + * 1.0 (the "Licenses"). You can select the license that you prefer but you may + * not use this file except in compliance with one of these Licenses. + * + * You can obtain a copy of the Apache 2.0 license at + * http://www.opensource.org/licenses/apache-2.0 + * + * You can obtain a copy of the LGPL 3.0 license at + * http://www.opensource.org/licenses/lgpl-3.0 + * + * You can obtain a copy of the LGPL 2.1 license at + * http://www.opensource.org/licenses/lgpl-2.1 + * + * You can obtain a copy of the CDDL 1.0 license at + * http://www.opensource.org/licenses/cddl1 + * + * You can obtain a copy of the EPL 1.0 license at + * http://www.opensource.org/licenses/eclipse-1.0 + * + * See the Licenses for the specific language governing permissions and + * limitations under the Licenses. + * + * Alternatively, you can obtain a royalty free commercial license with less + * limitations, transferable or non-transferable, directly at + * http://restlet.com/products/restlet-framework + * + * Restlet is a registered trademark of Restlet S.A.S. + */ + +package org.restlet.engine.util; + +import java.beans.BeanInfo; +import java.beans.PropertyDescriptor; +import java.util.HashMap; +import java.util.Map; + +/** + * Utilities to serialize {@link Throwable}. + * + * @author Manuel Boillod + */ +public class ThrowableSerializer { + + /** + * Serialize {@link Throwable} properties to a Map using reflection. + * The properties of {@link Throwable} class are ignored except if + * they are overriden. + * + * @param throwable + * {@link Throwable} instance to serialize. + * + * @return A map with the @link Throwable} subclasses properties. + */ + public static Map serializeToMap(Throwable throwable) { + try { + BeanInfo beanInfo = BeanInfoUtils.getBeanInfo(throwable.getClass()); + Map properties = new HashMap(); + + PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); + for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { + properties.put( + propertyDescriptor.getName(), + propertyDescriptor.getReadMethod().invoke(throwable)); + } + return properties; + } catch (Exception e) { + throw new RuntimeException("Could not serialize properties of class " + throwable.getCause(), e); + } + } +} diff --git a/modules/org.restlet/src/org/restlet/representation/StatusInfo.java b/modules/org.restlet/src/org/restlet/representation/StatusInfo.java new file mode 100644 index 0000000000..1d2aa6d5e4 --- /dev/null +++ b/modules/org.restlet/src/org/restlet/representation/StatusInfo.java @@ -0,0 +1,49 @@ +package org.restlet.representation; + +import org.restlet.data.Status; + +/** + * + * Representation of a {@link Status}. + * + * @author Manuel Boillod + */ +public class StatusInfo { + int code; + String reasonPhrase; + String description; + + public StatusInfo() { + } + + public StatusInfo(Status status) { + this.code = status.getCode(); + this.reasonPhrase = status.getReasonPhrase(); + this.description = status.getDescription(); + + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + public void setReasonPhrase(String reasonPhrase) { + this.reasonPhrase = reasonPhrase; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/modules/org.restlet/src/org/restlet/resource/Resource.java b/modules/org.restlet/src/org/restlet/resource/Resource.java index 5cfe7f4157..78475c3008 100644 --- a/modules/org.restlet/src/org/restlet/resource/Resource.java +++ b/modules/org.restlet/src/org/restlet/resource/Resource.java @@ -63,7 +63,6 @@ import org.restlet.representation.Representation; import org.restlet.representation.Variant; import org.restlet.service.MetadataService; -import org.restlet.service.StatusService; import org.restlet.util.Series; /** @@ -743,19 +742,20 @@ public Status getStatus() { return getResponse() == null ? null : getResponse().getStatus(); } + // [ifndef gwt] method /** * Returns the application's status service or create a new one. - * + * * @return The status service. */ - public StatusService getStatusService() { - StatusService result = null; + public org.restlet.service.StatusService getStatusService() { + org.restlet.service.StatusService result = null; // [ifndef gwt] instruction result = getApplication().getStatusService(); if (result == null) { - result = new StatusService(); + result = new org.restlet.service.StatusService(); } return result; diff --git a/modules/org.restlet/src/org/restlet/resource/ServerResource.java b/modules/org.restlet/src/org/restlet/resource/ServerResource.java index 963b6ead8f..d1cee60736 100644 --- a/modules/org.restlet/src/org/restlet/resource/ServerResource.java +++ b/modules/org.restlet/src/org/restlet/resource/ServerResource.java @@ -270,12 +270,6 @@ protected void doCatch(Throwable throwable) { if (getResponse() != null) { getResponse().setStatus(status); - - if (getResponseEntity() == null) { - getResponse().setEntity( - getStatusService().toRepresentation(status, throwable, - this)); - } } } diff --git a/modules/org.restlet/src/org/restlet/resource/Status.java b/modules/org.restlet/src/org/restlet/resource/Status.java index e55d3577e3..9a4f45adff 100644 --- a/modules/org.restlet/src/org/restlet/resource/Status.java +++ b/modules/org.restlet/src/org/restlet/resource/Status.java @@ -50,15 +50,23 @@ * @Get * public MyBean represent() throws MyServerError, MyNotFoundError; * - * @Status("500") + * @Status(500) * public class MyServerError implements Throwable{ * ... * } * - * @Status("404") + * @Status(404, serialize = false) * public class MyNotFoundError extends RuntimeException{ * ... * } + * + * @Status(value = 400) + * public class MyBadParameterError extends RuntimeException{ + * public String getParameterName() { + * ... + * }; + * ... + * } * * * @author Jerome Louvel @@ -70,10 +78,19 @@ /** * Specifies the HTTP status code associated to the annotated - * {@link Throwable}. + * {@link Throwable}. Default is 500. * * @return The result HTTP status code. */ - String value() default ""; + int value() default 500; + + /** + * Indicates if the annotated {@link Throwable} should be serialized in the + * HTTP response entity. + * + * @return True if {@link Throwable} should be serialized in the HTTP + * response entity. + */ + boolean serialize() default true; } diff --git a/modules/org.restlet/src/org/restlet/service/StatusService.java b/modules/org.restlet/src/org/restlet/service/StatusService.java index bb3b297a30..777c363b21 100644 --- a/modules/org.restlet/src/org/restlet/service/StatusService.java +++ b/modules/org.restlet/src/org/restlet/service/StatusService.java @@ -33,12 +33,19 @@ package org.restlet.service; +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; + import org.restlet.Context; import org.restlet.Request; import org.restlet.Response; +import org.restlet.data.MediaType; import org.restlet.data.Reference; import org.restlet.data.Status; import org.restlet.representation.Representation; +import org.restlet.representation.StatusInfo; +import org.restlet.representation.Variant; import org.restlet.resource.Resource; import org.restlet.resource.ResourceException; @@ -68,30 +75,76 @@ * @author Jerome Louvel */ public class StatusService extends Service { + + // [ifndef gwt] member + /** HTML Variant */ + private static final org.restlet.engine.resource.VariantInfo VARIANT_HTML = + new org.restlet.engine.resource.VariantInfo(MediaType.TEXT_HTML); + + // [ifndef gwt] member + /** The service used to select the preferred variant. */ + private volatile ConnegService connegService; + /** The email address to contact in case of error. */ private volatile String contactEmail; + // [ifndef gwt] member + /** The service used to convert between status/throwable and representation. */ + private volatile ConverterService converterService; + /** The home URI to propose in case of error. */ private volatile Reference homeRef; + // [ifndef gwt] member + /** The service used to select the preferred variant. */ + private volatile MetadataService metadataService; + /** True if an existing entity should be overwritten. */ private volatile boolean overwriting; /** - * Constructor. + * Constructor. By default, it creates the necessary services. */ public StatusService() { this(true); } + /** + * Constructor. By default, it creates the necessary services. + * + * @param enabled + * True if the service has been enabled. + * + */ + public StatusService(boolean enabled) { + // [ifndef gwt] instruction + this(enabled, new ConverterService(), new MetadataService(), + new ConnegService()); + // [ifdef gwt] instruction uncomment + // super(enabled); + // this.homeRef = new Reference("/"); + } + + // [ifndef gwt] method /** * Constructor. * * @param enabled * True if the service has been enabled. + * @param converterService + * The service used to convert between status/throwable and + * representation. + * @param metadataService + * The service used to select the preferred variant. + * @param connegService + * The service used to select the preferred variant. */ - public StatusService(boolean enabled) { + public StatusService(boolean enabled, ConverterService converterService, + MetadataService metadataService, ConnegService connegService) { super(enabled); + this.converterService = converterService; + this.metadataService = metadataService; + this.connegService = connegService; this.contactEmail = null; this.homeRef = new Reference("/"); this.overwriting = false; @@ -103,6 +156,15 @@ public org.restlet.routing.Filter createInboundFilter(Context context) { return new org.restlet.engine.application.StatusFilter(context, this); } + // [ifndef gwt] method + /** + * Returns the service used to select the preferred variant. + * @return The service used to select the preferred variant. + */ + public ConnegService getConnegService() { + return connegService; + } + /** * Returns the email address to contact in case of error. This is typically * used when creating the status representations. @@ -113,6 +175,15 @@ public String getContactEmail() { return this.contactEmail; } + // [ifndef gwt] method + /** + * Returns the service used to convert between status/throwable and representation. + * @return The service used to convert between status/throwable and representation. + */ + public ConverterService getConverterService() { + return converterService; + } + /** * Returns the home URI to propose in case of error. * @@ -122,10 +193,19 @@ public Reference getHomeRef() { return this.homeRef; } + // [ifndef gwt] method /** - * Returns a representation for the given status.
- * In order to customize the default representation, this method can be - * overridden. It returns null by default. + * Returns the service used to select the preferred variant. + * @return The service used to select the preferred variant. + */ + public MetadataService getMetadataService() { + return metadataService; + } + + /** + * Returns a representation for the given status. In order to customize the + * default representation, this method can be overridden. It returns null by + * default. * * @param status * The status to represent. @@ -134,16 +214,62 @@ public Reference getHomeRef() { * @param response * The response updated. * @return The representation of the given status. - * @deprecated Use {@link #toRepresentation(Status, Request, Response)} + * @deprecated Use + * {@link #toRepresentation(Status, Request, Response)} * instead. */ @Deprecated public Representation getRepresentation(Status status, Request request, Response response) { - // [ifndef gwt] instruction - return toRepresentation(status, null, request, response, null); - // [ifdef gwt] instruction uncomment - // return toRepresentation(status, null, request, response); + Representation result = null; + + // [ifndef gwt] + // do content negotiation for status + if (converterService != null && connegService != null + && metadataService != null) { + Object representationObject = null; + + // serialize exception if any and if {@link + // org.restlet.resource.Status} annotation ask for it + Throwable cause = status.getThrowable(); + if (cause != null) { + org.restlet.engine.resource.StatusAnnotationInfo sai = org.restlet.engine.resource.AnnotationUtils + .getInstance() + .getStatusAnnotationInfo(cause.getClass()); + if (sai != null && sai.isSerialize()) { + try { + representationObject = org.restlet.engine.util.ThrowableSerializer + .serializeToMap(cause); + } catch (Exception e) { + Context.getCurrentLogger().log( + Level.WARNING, + "Could not serialize throwable class " + + cause.getClass(), e); + } + } + } + + // default representation match with the status properties + if (representationObject == null) { + representationObject = new StatusInfo(status); + } + + List variants = org.restlet.engine.converter.ConverterUtils.getVariants( + representationObject.getClass(), null); + if (!variants.contains(VARIANT_HTML)) { + variants.add(VARIANT_HTML); + } + Variant variant = connegService.getPreferredVariant(variants, + request, metadataService); + try { + result = converterService.toRepresentation( + representationObject, variant); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + // [enddef] + return result; } /** @@ -166,7 +292,35 @@ public Representation getRepresentation(Status status, Request request, @Deprecated public Status getStatus(Throwable throwable, Request request, Response response) { - return toStatus(throwable, request, response); + Status result; + + Status defaultStatus = Status.SERVER_ERROR_INTERNAL; + Throwable t = throwable; + + // If throwable is a ResourceException, use its status and the cause. + if (throwable instanceof ResourceException) { + defaultStatus = ((ResourceException) throwable).getStatus(); + if (throwable.getCause() != null + && throwable.getCause() != throwable) { + t = throwable.getCause(); + } + } + + // [ifndef gwt] + // look for Status annotation + org.restlet.engine.resource.StatusAnnotationInfo sai = org.restlet.engine.resource.AnnotationUtils + .getInstance().getStatusAnnotationInfo(t.getClass()); + + if (sai != null) { + result = new Status(sai.getStatus(), t); + } else { + result = new Status(defaultStatus, t); + } + // [enddef] + // [ifdef gwt] instruction uncomment + // result = new Status(defaultStatus, t); + + return result; } /** @@ -184,7 +338,9 @@ public Status getStatus(Throwable throwable, Request request, */ @Deprecated public Status getStatus(Throwable throwable, Resource resource) { - return toStatus(throwable, resource); + return getStatus(throwable, + (resource == null) ? null : resource.getRequest(), + (resource == null) ? null : resource.getResponse()); } /** @@ -196,6 +352,15 @@ public boolean isOverwriting() { return this.overwriting; } + // [ifndef gwt] method + /** + * Sets the service used to select the preferred variant. + * @param connegService The service used to select the preferred variant. + */ + public void setConnegService(ConnegService connegService) { + this.connegService = connegService; + } + /** * Sets the email address to contact in case of error. This is typically * used when creating the status representations. @@ -207,6 +372,15 @@ public void setContactEmail(String contactEmail) { this.contactEmail = contactEmail; } + // [ifndef gwt] method + /** + * Sets the service used to convert between status/throwable and representation. + * @param converterService The service used to convert between status/throwable and representation. + */ + public void setConverterService(ConverterService converterService) { + this.converterService = converterService; + } + /** * Sets the home URI to propose in case of error. * @@ -217,6 +391,15 @@ public void setHomeRef(Reference homeRef) { this.homeRef = homeRef; } + // [ifndef gwt] method + /** + * Sets the service used to select the preferred variant. + * @param metadataService The service used to select the preferred variant. + */ + public void setMetadataService(MetadataService metadataService) { + this.metadataService = metadataService; + } + /** * Indicates if an existing entity should be overwritten. * @@ -228,75 +411,43 @@ public void setOverwriting(boolean overwriting) { } /** - * Returns a representation for the given status.
- * In order to customize the default representation, this method can be - * overridden. By default it invokes - * {@link #toRepresentation(Status, Request, Response)}. - * + * Returns a representation for the given status. In order to customize the + * default representation, this method can be overridden. It returns a + * {@link org.restlet.data.Status} representation by default or a + * {@link java.lang.Throwable} representation if the throwable is annotated + * with {@link org.restlet.resource.Status}. + * * @param status * The status to represent. - * @param throwable - * The exception or error caught. - * @param resource - * The parent resource. + * @param request + * The request handled. + * @param response + * The response updated. * @return The representation of the given status. */ - public Representation toRepresentation(Status status, Throwable throwable, - Resource resource) { - // [ifndef gwt] instruction - return toRepresentation(status, throwable, resource.getRequest(), - resource.getResponse(), resource.getConverterService()); - // [ifdef gwt] instruction uncomment - // return null; + public Representation toRepresentation(Status status, + Request request, Response response) { + return getRepresentation(status, request, response); } - // [ifndef gwt] method /** - * Returns a representation for the given status. In order to customize the - * default representation, this method can be overridden. It returns null by - * default. + * Returns a representation for the given status.
+ * In order to customize the default representation, this method can be + * overridden. By default it invokes + * {@link #toRepresentation(Status, Request, Response)} * * @param status * The status to represent. - * @param throwable - * The exception or error caught. - * @param request - * The request handled. - * @param response - * The response updated. - * @param converterService - * The converter service. + * @param resource + * The parent resource. * @return The representation of the given status. */ - public Representation toRepresentation(Status status, Throwable throwable, - Request request, Response response, - ConverterService converterService) { - return null; + public Representation toRepresentation(Status status, + Resource resource) { + return toRepresentation(status, resource.getRequest(), + resource.getResponse()); } - // [ifdef gwt] method uncomment - // /** - // * Returns a representation for the given status. In order to customize - // the - // * default representation, this method can be overridden. It returns null - // by - // * default. - // * - // * @param status - // * The status to represent. - // * @param throwable - // * The exception or error caught. - // * @param request - // * The request handled. - // * @param response - // * The response updated. - // * @return The representation of the given status. - // */ - // public Representation toRepresentation(Status status, Throwable - // throwable, - // Request request, Response response) { - // return null; - // } /** * Returns a status for a given exception or error. By default it unwraps @@ -315,36 +466,8 @@ public Representation toRepresentation(Status status, Throwable throwable, * @return The representation of the given status. */ public Status toStatus(Throwable throwable, Request request, - Response response) { - Status result; - - Status defaultStatus = Status.SERVER_ERROR_INTERNAL; - Throwable t = throwable; - - //If throwable is a ResourceException, use its status and the cause. - if (throwable instanceof ResourceException) { - defaultStatus = ((ResourceException) throwable).getStatus(); - if (throwable.getCause() != null && throwable.getCause() != throwable) { - t = throwable.getCause(); - } - } - - // [ifndef gwt] - // look for Status annotation - org.restlet.engine.resource.StatusAnnotationInfo sai = org.restlet.engine.resource.AnnotationUtils - .getInstance() - .getStatusAnnotationInfo(t.getClass()); - - if (sai != null) { - result = new Status(sai.getStatus(), t); - } else { - result = new Status(defaultStatus, t); - } - // [enddef] - // [ifdef gwt] instruction uncomment - // result = new Status(defaultStatus, t); - - return result; + Response response) { + return getStatus(throwable, request, response); } /** @@ -360,21 +483,6 @@ public Status toStatus(Throwable throwable, Request request, * @return The representation of the given status. */ public Status toStatus(Throwable throwable, Resource resource) { - return toStatus(throwable, - (resource == null) ? null : resource.getRequest(), - (resource == null) ? null : resource.getResponse()); - } - - /** - * - * @param status - * @param representation - * @return - */ - public Throwable toThrowable(Status status, Representation representation) { - Throwable result = null; - - return result; + return getStatus(throwable, resource); } - }