From 21d058f86be94cffe329465adf26b38d235c95c3 Mon Sep 17 00:00:00 2001 From: "b.debeaubien" Date: Mon, 10 Nov 2014 17:50:49 -0500 Subject: [PATCH] Added @Destroy annotation, which allows ResourceProviders to do cleanup when the server shuts down; renamed "ResfulServer" tests to "RestfulServer" --- .../ca/uhn/fhir/rest/annotation/Destroy.java | 36 +++++ .../fhir/rest/method/BaseMethodBinding.java | 14 +- .../uhn/fhir/rest/server/RestfulServer.java | 131 ++++++++++-------- .../ca/uhn/fhir/rest/server/CorsTest.java | 2 +- .../ca/uhn/fhir/rest/server/DestroyTest.java | 95 +++++++++++++ ...Test.java => RestfulServerMethodTest.java} | 4 +- ...va => RestfulServerSelfReferenceTest.java} | 4 +- 7 files changed, 211 insertions(+), 75 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Destroy.java create mode 100644 hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/DestroyTest.java rename hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/{ResfulServerMethodTest.java => RestfulServerMethodTest.java} (99%) rename hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/{ResfulServerSelfReferenceTest.java => RestfulServerSelfReferenceTest.java} (98%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Destroy.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Destroy.java new file mode 100644 index 00000000000..52c6595d901 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Destroy.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.rest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +/** + * ResourceProvider methods tagged with @Destroy will be invoked when the RestfulServer is shut down. + * This is your chance to do any cleanup. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Destroy { +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java index 648fc2e5730..56a11a432d2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java @@ -33,6 +33,7 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; +import ca.uhn.fhir.rest.annotation.*; import org.apache.commons.io.IOUtils; import ca.uhn.fhir.context.ConfigurationException; @@ -44,18 +45,6 @@ import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum; import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum; import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.annotation.AddTags; -import ca.uhn.fhir.rest.annotation.Create; -import ca.uhn.fhir.rest.annotation.Delete; -import ca.uhn.fhir.rest.annotation.DeleteTags; -import ca.uhn.fhir.rest.annotation.GetTags; -import ca.uhn.fhir.rest.annotation.History; -import ca.uhn.fhir.rest.annotation.Metadata; -import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.annotation.Search; -import ca.uhn.fhir.rest.annotation.Transaction; -import ca.uhn.fhir.rest.annotation.Update; -import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; @@ -257,6 +246,7 @@ public abstract class BaseMethodBinding implements IClientResponseHandler AddTags addTags = theMethod.getAnnotation(AddTags.class); DeleteTags deleteTags = theMethod.getAnnotation(DeleteTags.class); Transaction transaction = theMethod.getAnnotation(Transaction.class); + // ** if you add another annotation above, also add it to the next line: if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction)) { return null; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 5c671c34a96..de1d32ae372 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -20,48 +20,10 @@ package ca.uhn.fhir.rest.server; * #L% */ -import static org.apache.commons.lang3.StringUtils.isNotBlank; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.UUID; -import java.util.zip.GZIPOutputStream; - -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import ca.uhn.fhir.context.ProvidedResourceScanner; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.exception.ExceptionUtils; - import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.ProvidedResourceScanner; import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.model.api.Bundle; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.api.Tag; -import ca.uhn.fhir.model.api.TagList; +import ca.uhn.fhir.model.api.*; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome.BaseIssue; import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; @@ -69,13 +31,9 @@ import ca.uhn.fhir.model.dstu.resource.Binary; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.annotation.Destroy; import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.method.BaseMethodBinding; -import ca.uhn.fhir.rest.method.ConformanceMethodBinding; -import ca.uhn.fhir.rest.method.OtherOperationTypeEnum; -import ca.uhn.fhir.rest.method.Request; -import ca.uhn.fhir.rest.method.RequestDetails; -import ca.uhn.fhir.rest.method.SearchMethodBinding; +import ca.uhn.fhir.rest.method.*; import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -83,6 +41,28 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.util.VersionUtil; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.exception.ExceptionUtils; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URLEncoder; +import java.util.*; +import java.util.zip.GZIPOutputStream; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; public class RestfulServer extends HttpServlet { @@ -280,9 +260,35 @@ public class RestfulServer extends HttpServlet { } } + private void invokeDestroy(Object theProvider) { + Class clazz = theProvider.getClass(); + invokeDestroy(theProvider, clazz); + } + + private void invokeDestroy(Object theProvider, Class clazz) { + for (Method m : clazz.getDeclaredMethods()) { + Destroy destroy = m.getAnnotation(Destroy.class); + if (destroy != null) { + try { + m.invoke(theProvider); + } catch (IllegalAccessException e) { + ourLog.error("Exception occurred in destroy ", e); + } catch (InvocationTargetException e) { + ourLog.error("Exception occurred in destroy ", e); + } + return; + } + } + + Class supertype = clazz.getSuperclass(); + if (!Object.class.equals(supertype)) { + invokeDestroy(theProvider, supertype); + } + } + /** * Returns the setting for automatically adding profile tags - * + * * @see #setAddProfileTag(AddProfileTagEnum) */ public AddProfileTagEnum getAddProfileTag() { @@ -314,7 +320,7 @@ public class RestfulServer extends HttpServlet { /** * Provides the non-resource specific providers which implement method calls on this server - * + * * @see #getResourceProviders() */ public Collection getPlainProviders() { @@ -351,7 +357,7 @@ public class RestfulServer extends HttpServlet { /** * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, but can be helpful to set with something appropriate. - * + * * @see RestfulServer#setServerName(String) */ public String getServerName() { @@ -642,7 +648,7 @@ public class RestfulServer extends HttpServlet { return; } } - + BaseOperationOutcome oo = null; int statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR; @@ -761,7 +767,16 @@ public class RestfulServer extends HttpServlet { // nothing by default } - public boolean isUseBrowserFriendlyContentTypes() { + @Override + public void destroy() { + if (getResourceProviders() != null) { + for (IResourceProvider iResourceProvider : getResourceProviders()) { + invokeDestroy(iResourceProvider); + } + } + } + + public boolean isUseBrowserFriendlyContentTypes() { return myUseBrowserFriendlyContentTypes; } @@ -778,7 +793,7 @@ public class RestfulServer extends HttpServlet { /** * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} (which is the default), the server will automatically add a profile tag based * on the class of the resource(s) being returned. - * + * * @param theAddProfileTag * The behaviour enum (must not be null) */ @@ -798,7 +813,7 @@ public class RestfulServer extends HttpServlet { /** * Sets (or clears) the list of interceptors - * + * * @param theList * The list of interceptors (may be null) */ @@ -811,7 +826,7 @@ public class RestfulServer extends HttpServlet { /** * Sets (or clears) the list of interceptors - * + * * @param theList * The list of interceptors (may be null) */ @@ -831,7 +846,7 @@ public class RestfulServer extends HttpServlet { /** * Sets the non-resource specific providers which implement method calls on this server. - * + * * @see #setResourceProviders(Collection) */ public void setPlainProviders(Collection theProviders) { @@ -840,7 +855,7 @@ public class RestfulServer extends HttpServlet { /** * Sets the non-resource specific providers which implement method calls on this server. - * + * * @see #setResourceProviders(Collection) */ public void setPlainProviders(Object... theProv) { @@ -849,7 +864,7 @@ public class RestfulServer extends HttpServlet { /** * Sets the non-resource specific providers which implement method calls on this server - * + * * @see #setResourceProviders(Collection) */ public void setProviders(Object... theProviders) { @@ -884,7 +899,7 @@ public class RestfulServer extends HttpServlet { * By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to null if you do not wish to export a conformance statement. *

* Note that this method can only be called before the server is initialized. - * + * * @throws IllegalStateException * Note that this method can only be called prior to {@link #init() initialization} and will throw an {@link IllegalStateException} if called after that. */ diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java index f2220d4039e..37c5ac7e847 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/CorsTest.java @@ -29,7 +29,7 @@ import org.junit.Test; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.dstu.resource.Patient; -import ca.uhn.fhir.rest.server.ResfulServerSelfReferenceTest.DummyPatientResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServerSelfReferenceTest.DummyPatientResourceProvider; import ca.uhn.fhir.util.PortUtil; /** diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/DestroyTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/DestroyTest.java new file mode 100644 index 00000000000..a41085cb5d3 --- /dev/null +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/DestroyTest.java @@ -0,0 +1,95 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.dstu.resource.DiagnosticReport; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.Destroy; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.MethodOutcome; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import java.util.Arrays; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Created by Bill de Beaubien on 11/10/2014. + */ + +@RunWith(MockitoJUnitRunner.class) +public class DestroyTest { + + @Test + public void testDestroyCallsAnnotatedMethodsOnProviders() throws ServletException { + RestfulServer servlet = new RestfulServer(); + DiagnosticReportProvider provider = spy(new DiagnosticReportProvider()); + servlet.setResourceProviders(Arrays.asList((IResourceProvider) provider)); + servlet.init(); + servlet.destroy(); + verify(provider).destroy(); + } + + @Test + public void testChainsUpThroughSuperclasses() throws ServletException { + RestfulServer servlet = new RestfulServer(); + DerivedDiagnosticReportProvider provider = spy(new DerivedDiagnosticReportProvider()); + servlet.setResourceProviders(Arrays.asList((IResourceProvider) provider)); + servlet.init(); + servlet.destroy(); + verify(provider).destroy(); + } + + @Test + public void testNoDestroyDoesNotCauseInfiniteRecursion() throws ServletException { + RestfulServer servlet = new RestfulServer(); + DiagnosticReportProviderSansDestroy provider = new DiagnosticReportProviderSansDestroy(); + servlet.setResourceProviders(Arrays.asList((IResourceProvider) provider)); + servlet.init(); + servlet.destroy(); + // nothing to verify other than the test didn't hang forever + } + + public class DiagnosticReportProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return DiagnosticReport.class; + } + + @Create + public MethodOutcome createResource(@ResourceParam DiagnosticReport theDiagnosticReport) { + // do nothing + return new MethodOutcome(); + } + + @Destroy + public void destroy() { + // do nothing + } + } + + public class DerivedDiagnosticReportProvider extends DiagnosticReportProvider { + // move along, nothing to see here + } + + public class DiagnosticReportProviderSansDestroy implements IResourceProvider { + + @Override + public Class getResourceType() { + return DiagnosticReport.class; + } + + @Create + public MethodOutcome createResource(@ResourceParam DiagnosticReport theDiagnosticReport) { + // do nothing + return new MethodOutcome(); + } + } +} diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/RestfulServerMethodTest.java similarity index 99% rename from hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java rename to hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/RestfulServerMethodTest.java index aea82116b0e..9f48e00acc6 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/RestfulServerMethodTest.java @@ -75,11 +75,11 @@ import ca.uhn.fhir.util.PortUtil; /** * Created by dsotnikov on 2/25/2014. */ -public class ResfulServerMethodTest { +public class RestfulServerMethodTest { private static CloseableHttpClient ourClient; private static FhirContext ourCtx; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResfulServerMethodTest.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerMethodTest.class); private static int ourPort; private static DummyDiagnosticReportResourceProvider ourReportProvider; private static Server ourServer; diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResfulServerSelfReferenceTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/RestfulServerSelfReferenceTest.java similarity index 98% rename from hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResfulServerSelfReferenceTest.java rename to hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/RestfulServerSelfReferenceTest.java index cefba40d599..8616ec7d97c 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ResfulServerSelfReferenceTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/RestfulServerSelfReferenceTest.java @@ -41,9 +41,9 @@ import ca.uhn.fhir.util.PortUtil; /** * Created by dsotnikov on 2/25/2014. */ -public class ResfulServerSelfReferenceTest { +public class RestfulServerSelfReferenceTest { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResfulServerSelfReferenceTest.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServerSelfReferenceTest.class); private static CloseableHttpClient ourClient; private static FhirContext ourCtx;