Changes in preparation for the m2 model 30/97430/4
authorStraubs, Ralph (rs8887) <rs8887@att.com>
Tue, 22 Oct 2019 15:43:09 +0000 (10:43 -0500)
committerStraubs, Ralph (rs8887) <rs8887@att.com>
Thu, 24 Oct 2019 16:09:45 +0000 (11:09 -0500)
1) Replace enum 'ControlLoopTargetType' with 'String' -- symbols such as
   'ControlLoopTargetType.VM' still work, but now expand into a String.
   This gives the ability to add new application-specific types.

2) Move 'ControlLoopEvent.payload' to 'VirtualControlLoopEvent'. This
   symbol isn't common to all 'ControlLoopEvent' types, and this
   definition collides with a 'payload' defined in our application.

3) Add 'RestManager.patch(...)', which performs a REST patch.

4) Add lombok getters and setters

5) Remove trailing spaces

6) Fix order of arguments in 'assertEquals' in 'ControlLoopTargetTypeTest'

7) Update Junit tests in 'RestTest' to include 'RestManager.patch(...)'

Issue-ID: POLICY-1948
Signed-off-by: Straubs, Ralph (rs8887) <rs8887@att.com>
Change-Id: I08e04ea3cbcf368c760b630bcfe23a4370cf94dc

models-interactions/model-impl/events/src/main/java/org/onap/policy/controlloop/ControlLoopEvent.java
models-interactions/model-impl/events/src/main/java/org/onap/policy/controlloop/ControlLoopNotification.java
models-interactions/model-impl/events/src/main/java/org/onap/policy/controlloop/ControlLoopTargetType.java
models-interactions/model-impl/events/src/main/java/org/onap/policy/controlloop/VirtualControlLoopEvent.java
models-interactions/model-impl/events/src/main/java/org/onap/policy/controlloop/util/Serialization.java
models-interactions/model-impl/events/src/test/java/org/onap/policy/controlloop/ControlLoopEventTest.java
models-interactions/model-impl/events/src/test/java/org/onap/policy/controlloop/ControlLoopTargetTypeTest.java
models-interactions/model-impl/events/src/test/java/org/onap/policy/controlloop/VirtualControlLoopEventTest.java
models-interactions/model-impl/rest/src/main/java/org/onap/policy/rest/RestManager.java
models-interactions/model-impl/rest/src/test/java/org/onap/policy/rest/RestTest.java

index 43cd640..534e843 100644 (file)
@@ -24,47 +24,48 @@ package org.onap.policy.controlloop;
 import com.google.gson.annotations.SerializedName;
 import java.io.Serializable;
 import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
 
+@Getter
+@Setter
 public abstract class ControlLoopEvent implements Serializable {
 
     private static final long serialVersionUID = 2391252138583119195L;
-    
+
     @SerializedName("closedLoopControlName")
     private String closedLoopControlName;
-    
+
     @SerializedName("version")
     private String version = "1.0.2";
-    
+
     @SerializedName("requestID")
     private UUID requestId;
-    
+
     @SerializedName("closedLoopEventClient")
     private String closedLoopEventClient;
-    
+
     @SerializedName("target_type")
-    private ControlLoopTargetType targetType;
-    
+    private String targetType;
+
     @SerializedName("target")
     private String target;
-    
+
     @SerializedName("from")
     private String from;
-    
+
     @SerializedName("policyScope")
     private String policyScope;
-    
+
     @SerializedName("policyName")
     private String policyName;
-    
+
     @SerializedName("policyVersion")
     private String policyVersion;
-    
+
     @SerializedName("closedLoopEventStatus")
     private ControlLoopEventStatus closedLoopEventStatus;
 
-    @SerializedName("payload")
-    private String payload;
-    
     public ControlLoopEvent() {
 
     }
@@ -88,106 +89,9 @@ public abstract class ControlLoopEvent implements Serializable {
         this.policyName = event.policyName;
         this.policyVersion = event.policyVersion;
         this.closedLoopEventStatus = event.closedLoopEventStatus;
-        this.payload = event.payload;
     }
 
     public boolean isEventStatusValid() {
         return this.closedLoopEventStatus != null;
     }
-
-    public String getClosedLoopControlName() {
-        return closedLoopControlName;
-    }
-
-    public void setClosedLoopControlName(String closedLoopControlName) {
-        this.closedLoopControlName = closedLoopControlName;
-    }
-
-    public String getVersion() {
-        return version;
-    }
-
-    public void setVersion(String version) {
-        this.version = version;
-    }
-
-    public UUID getRequestId() {
-        return requestId;
-    }
-
-    public void setRequestId(UUID requestId) {
-        this.requestId = requestId;
-    }
-
-    public String getClosedLoopEventClient() {
-        return closedLoopEventClient;
-    }
-
-    public void setClosedLoopEventClient(String closedLoopEventClient) {
-        this.closedLoopEventClient = closedLoopEventClient;
-    }
-
-    public ControlLoopTargetType getTargetType() {
-        return targetType;
-    }
-
-    public void setTargetType(ControlLoopTargetType targetType) {
-        this.targetType = targetType;
-    }
-
-    public String getTarget() {
-        return target;
-    }
-
-    public void setTarget(String target) {
-        this.target = target;
-    }
-
-    public String getFrom() {
-        return from;
-    }
-
-    public void setFrom(String from) {
-        this.from = from;
-    }
-
-    public String getPolicyScope() {
-        return policyScope;
-    }
-
-    public void setPolicyScope(String policyScope) {
-        this.policyScope = policyScope;
-    }
-
-    public String getPolicyName() {
-        return policyName;
-    }
-
-    public void setPolicyName(String policyName) {
-        this.policyName = policyName;
-    }
-
-    public String getPolicyVersion() {
-        return policyVersion;
-    }
-
-    public void setPolicyVersion(String policyVersion) {
-        this.policyVersion = policyVersion;
-    }
-
-    public ControlLoopEventStatus getClosedLoopEventStatus() {
-        return closedLoopEventStatus;
-    }
-
-    public void setClosedLoopEventStatus(ControlLoopEventStatus closedLoopEventStatus) {
-        this.closedLoopEventStatus = closedLoopEventStatus;
-    }
-
-    public String getPayload() {
-        return payload;
-    }
-
-    public void setPayload(String payload) {
-        this.payload = payload;
-    }
 }
index dd1854a..9242917 100644 (file)
@@ -27,7 +27,11 @@ import java.time.ZonedDateTime;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
 
+@Getter
+@Setter
 public abstract class ControlLoopNotification implements Serializable {
 
     private static final long serialVersionUID = 7538596984567127915L;
@@ -36,7 +40,7 @@ public abstract class ControlLoopNotification implements Serializable {
     private String version = "1.0.2";
     private UUID requestId;
     private String closedLoopEventClient;
-    private ControlLoopTargetType targetType;
+    private String targetType;
     private String target;
     private String from;
     private String policyScope;
@@ -68,124 +72,4 @@ public abstract class ControlLoopNotification implements Serializable {
         this.setTargetType(event.getTargetType());
         this.setTarget(event.getTarget());
     }
-
-    public String getClosedLoopControlName() {
-        return closedLoopControlName;
-    }
-
-    public void setClosedLoopControlName(String closedLoopControlName) {
-        this.closedLoopControlName = closedLoopControlName;
-    }
-
-    public String getVersion() {
-        return version;
-    }
-
-    public void setVersion(String version) {
-        this.version = version;
-    }
-
-    public UUID getRequestId() {
-        return requestId;
-    }
-
-    public void setRequestId(UUID requestId) {
-        this.requestId = requestId;
-    }
-
-    public String getClosedLoopEventClient() {
-        return closedLoopEventClient;
-    }
-
-    public void setClosedLoopEventClient(String closedLoopEventClient) {
-        this.closedLoopEventClient = closedLoopEventClient;
-    }
-
-    public ControlLoopTargetType getTargetType() {
-        return targetType;
-    }
-
-    public void setTargetType(ControlLoopTargetType targetType) {
-        this.targetType = targetType;
-    }
-
-    public String getTarget() {
-        return target;
-    }
-
-    public void setTarget(String target) {
-        this.target = target;
-    }
-
-    public String getFrom() {
-        return from;
-    }
-
-    public void setFrom(String from) {
-        this.from = from;
-    }
-
-    public String getPolicyScope() {
-        return policyScope;
-    }
-
-    public void setPolicyScope(String policyScope) {
-        this.policyScope = policyScope;
-    }
-
-    public String getPolicyName() {
-        return policyName;
-    }
-
-    public void setPolicyName(String policyName) {
-        this.policyName = policyName;
-    }
-
-    public String getPolicyVersion() {
-        return policyVersion;
-    }
-
-    public void setPolicyVersion(String policyVersion) {
-        this.policyVersion = policyVersion;
-    }
-
-    public ControlLoopNotificationType getNotification() {
-        return notification;
-    }
-
-    public void setNotification(ControlLoopNotificationType notification) {
-        this.notification = notification;
-    }
-
-    public String getMessage() {
-        return message;
-    }
-
-    public void setMessage(String message) {
-        this.message = message;
-    }
-
-    public ZonedDateTime getNotificationTime() {
-        return notificationTime;
-    }
-
-    public void setNotificationTime(ZonedDateTime notificationTime) {
-        this.notificationTime = notificationTime;
-    }
-
-    public Integer getOpsClTimer() {
-        return opsClTimer;
-    }
-
-    public void setOpsClTimer(Integer opsClTimer) {
-        this.opsClTimer = opsClTimer;
-    }
-
-    public List<ControlLoopOperation> getHistory() {
-        return history;
-    }
-
-    public void setHistory(List<ControlLoopOperation> history) {
-        this.history = history;
-    }
 }
index fda0d08..1ca1825 100644 (file)
 
 package org.onap.policy.controlloop;
 
-public enum ControlLoopTargetType {
-    VM("VM"), VF("VF"), VFC("VFC"), VNF("VNF");
-
-    private String type;
-
-    private ControlLoopTargetType(String type) {
-        this.type = type;
-    }
-
-    @Override
-    public String toString() {
-        return this.type;
-    }
-
-    /**
-     * Convert a String type to a ControlLoopTargetType.
-     *
-     * @param type the String type
-     * @return the ControlLoopTargetType
-     */
-    public static ControlLoopTargetType toType(String type) {
-        if (VM.toString().equals(type)) {
-            return VM;
-        }
-        if (VF.toString().equals(type)) {
-            return VF;
-        }
-        if (VFC.toString().equals(type)) {
-            return VFC;
-        }
-        if (VNF.toString().equals(type)) {
-            return VNF;
-        }
-
-        return null;
-    }
+public class ControlLoopTargetType {
+    public static final String VM = "VM";
+    public static final String VF = "VF";
+    public static final String VFC = "VFC";
+    public static final String VNF = "VNF";
 }
index 1a09691..43026a2 100644 (file)
@@ -26,17 +26,24 @@ import com.google.gson.annotations.SerializedName;
 import java.time.Instant;
 import java.util.HashMap;
 import java.util.Map;
+import lombok.Getter;
+import lombok.Setter;
 
+@Getter
+@Setter
 public class VirtualControlLoopEvent extends ControlLoopEvent {
 
     private static final long serialVersionUID = -5752405682246066226L;
-    
+
+    @SerializedName("payload")
+    private String payload;
+
     @SerializedName("closedLoopAlarmStart")
     private Instant closedLoopAlarmStart;
-    
+
     @SerializedName("closedLoopAlarmEnd")
     private Instant closedLoopAlarmEnd;
-    
+
     @SerializedName("AAI")
     private Map<String, String> aai = new HashMap<>();
 
@@ -56,31 +63,8 @@ public class VirtualControlLoopEvent extends ControlLoopEvent {
         if (event.aai != null) {
             this.aai = new HashMap<>(event.aai);
         }
+        this.payload = event.payload;
         this.closedLoopAlarmStart = event.closedLoopAlarmStart;
         this.closedLoopAlarmEnd = event.closedLoopAlarmEnd;
     }
-
-    public Instant getClosedLoopAlarmStart() {
-        return closedLoopAlarmStart;
-    }
-
-    public void setClosedLoopAlarmStart(Instant closedLoopAlarmStart) {
-        this.closedLoopAlarmStart = closedLoopAlarmStart;
-    }
-
-    public Instant getClosedLoopAlarmEnd() {
-        return closedLoopAlarmEnd;
-    }
-
-    public void setClosedLoopAlarmEnd(Instant closedLoopAlarmEnd) {
-        this.closedLoopAlarmEnd = closedLoopAlarmEnd;
-    }
-
-    public Map<String, String> getAai() {
-        return aai;
-    }
-
-    public void setAai(Map<String, String> aai) {
-        this.aai = aai;
-    }
 }
index df7dc54..96797ed 100644 (file)
@@ -36,7 +36,6 @@ import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 
 import org.onap.policy.controlloop.ControlLoopNotificationType;
-import org.onap.policy.controlloop.ControlLoopTargetType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -45,19 +44,19 @@ public final class Serialization {
             new GsonBuilder().disableHtmlEscaping().registerTypeAdapter(ZonedDateTime.class, new GsonUtcAdapter())
                     .registerTypeAdapter(Instant.class, new GsonInstantAdapter())
                     .registerTypeAdapter(ControlLoopNotificationType.class, new NotificationTypeAdapter())
-                    .registerTypeAdapter(ControlLoopTargetType.class, new TargetTypeAdapter()).create();
+                    .create();
 
 
     public static final Gson gsonPretty = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting()
             .registerTypeAdapter(ZonedDateTime.class, new GsonUtcAdapter())
             .registerTypeAdapter(Instant.class, new GsonInstantAdapter())
             .registerTypeAdapter(ControlLoopNotificationType.class, new NotificationTypeAdapter())
-            .registerTypeAdapter(ControlLoopTargetType.class, new TargetTypeAdapter()).create();
+            .create();
 
     public static final Gson gsonJunit = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting()
             .registerTypeAdapter(ZonedDateTime.class, new GsonUtcAdapter())
             .registerTypeAdapter(Instant.class, new GsonInstantAdapter())
-            .registerTypeAdapter(ControlLoopTargetType.class, new TargetTypeAdapter()).create();
+            .create();
 
     private Serialization() {}
 
@@ -76,19 +75,6 @@ public final class Serialization {
         }
     }
 
-    public static class TargetTypeAdapter
-            implements JsonSerializer<ControlLoopTargetType>, JsonDeserializer<ControlLoopTargetType> {
-        @Override
-        public JsonElement serialize(ControlLoopTargetType src, Type typeOfSrc, JsonSerializationContext context) {
-            return new JsonPrimitive(src.toString());
-        }
-
-        @Override
-        public ControlLoopTargetType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
-            return ControlLoopTargetType.toType(json.getAsString());
-        }
-    }
-
     public static class GsonUtcAdapter implements JsonSerializer<ZonedDateTime>, JsonDeserializer<ZonedDateTime> {
         private static final Logger logger = LoggerFactory.getLogger(GsonUtcAdapter.class);
         public static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSxxx");
index feaf22e..2def975 100644 (file)
@@ -63,9 +63,6 @@ public class ControlLoopEventTest {
         event.setFrom("from");
         assertEquals("from", event.getFrom());
 
-        event.setPayload("payload");
-        assertEquals("payload", event.getPayload());
-
         event.setPolicyName("policyname");
         assertEquals("policyname", event.getPolicyName());
 
index d1412fe..3434a59 100644 (file)
@@ -30,11 +30,9 @@ public class ControlLoopTargetTypeTest {
 
     @Test
     public void test() {
-        assertEquals(ControlLoopTargetType.VM, ControlLoopTargetType.toType("VM"));
-        assertEquals(ControlLoopTargetType.VF, ControlLoopTargetType.toType("VF"));
-        assertEquals(ControlLoopTargetType.VFC, ControlLoopTargetType.toType("VFC"));
-        assertEquals(ControlLoopTargetType.VNF, ControlLoopTargetType.toType("VNF"));
-
-        assertNull(ControlLoopTargetType.toType("foo"));
+        assertEquals("VM", ControlLoopTargetType.VM);
+        assertEquals("VF", ControlLoopTargetType.VF);
+        assertEquals("VFC", ControlLoopTargetType.VFC);
+        assertEquals("VNF", ControlLoopTargetType.VNF);
     }
 }
index 2acd303..3fc7d13 100644 (file)
@@ -37,6 +37,9 @@ public class VirtualControlLoopEventTest {
         assertNotNull(event);
         assertNotNull(event.getAai());
 
+        event.setPayload("payload");
+        assertEquals("payload", event.getPayload());
+
         Instant now = Instant.now();
         event.setClosedLoopAlarmStart(now);
         event.setClosedLoopAlarmEnd(now);
index 643c629..dde3aa2 100644 (file)
@@ -29,6 +29,7 @@ import org.apache.http.HttpHeaders;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.methods.HttpRequestBase;
@@ -171,6 +172,33 @@ public class RestManager {
         return sendRequest(delete);
     }
 
+    /**
+     * Perform REST Patch.
+     *
+     * @param url         the url
+     * @param username    the user name
+     * @param password    the password
+     * @param headers     any headers
+     * @param body        body to send
+     * @return the response status code and the body
+     */
+    public Pair<Integer, String> patch(String url, String username, String password,
+                                       Map<String, String> headers, String body) {
+        String contentType = "application/merge-patch+json";
+        HttpPatch patch = new HttpPatch(url);
+        addHeaders(patch, username, password, headers);
+        patch.addHeader(CONTENT_TYPE, contentType);
+        try {
+            StringEntity input = new StringEntity(body);
+            input.setContentType(contentType);
+            patch.setEntity(input);
+        } catch (Exception e) {
+            logger.error("patch threw: ", e);
+            return null;
+        }
+        return sendRequest(patch);
+    }
+
     /**
      * Send REST request.
      *
index 903ec2f..21a9716 100644 (file)
@@ -24,10 +24,17 @@ package org.onap.policy.rest;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
+import javax.ws.rs.HttpMethod;
 import javax.ws.rs.POST;
 import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
@@ -45,7 +52,7 @@ import org.onap.policy.rest.RestManager.Pair;
 
 @Path("RestTest")
 public class RestTest {
-
+    private static final String MERGE_PATCH_PLUS_JSON = "application/merge-patch+json";
 
     private static final String NAME_PARAM = "Bob";
     private static final String AGE_PARAM = "10";
@@ -64,6 +71,8 @@ public class RestTest {
     private static String putUriBlank;
     private static String postUri;
     private static String postUriBlank;
+    private static String patchUri;
+    private static String patchUriBlank;
 
     private static HttpServletServer server;
 
@@ -81,6 +90,8 @@ public class RestTest {
         putUriBlank = baseUri + "RestTest/PutBlank";
         postUri = baseUri + "RestTest/PostHello/" + NAME_PARAM + "?age=" + AGE_PARAM;
         postUriBlank = baseUri + "RestTest/PostBlank";
+        patchUri = baseUri + "RestTest/PatchHello/" + NAME_PARAM + "?age=" + AGE_PARAM;
+        patchUriBlank = baseUri + "RestTest/PatchBlank";
 
         server = HttpServletServerFactoryInstance.getServerFactory()
             .build("RestTest", LOCALHOST, port, "/" + BASE, false, true);
@@ -123,6 +134,12 @@ public class RestTest {
         mgr.delete(null, "user", null, null, null, null);
     }
 
+    @Test(expected = NullPointerException.class)
+    public void testPatchUrlNull() {
+        RestManager mgr = new RestManager();
+        mgr.patch(null, "user", null, null, PAYLOAD);
+    }
+
     @Test
     public void testUsernameNull() {
         RestManager mgr = new RestManager();
@@ -168,6 +185,18 @@ public class RestTest {
         assertTrue(result.second != null);
         assertTrue(result.second.length() > 0);
         assertEquals("POST: " + PAYLOAD + RETURN_STRING, result.second);
+
+        result = mgr.patch(patchUri, null, null, null, PAYLOAD);
+        assertEquals((Integer)200, result.first);
+        assertTrue(result.second != null);
+        assertTrue(result.second.length() > 0);
+        assertEquals("PATCH: " + PAYLOAD + EXPECT_STRING, result.second);
+
+        result = mgr.patch(patchUriBlank, null, null, null, PAYLOAD);
+        assertEquals((Integer)200, result.first);
+        assertTrue(result.second != null);
+        assertTrue(result.second.length() > 0);
+        assertEquals("PATCH: " + PAYLOAD + RETURN_STRING, result.second);
     }
 
     @Test
@@ -209,6 +238,18 @@ public class RestTest {
         assertTrue(result.second != null);
         assertTrue(result.second.length() > 0);
         assertEquals("POST: " + PAYLOAD + RETURN_STRING, result.second);
+
+        result = mgr.patch(patchUri, "", null, null, PAYLOAD);
+        assertEquals((Integer)200, result.first);
+        assertTrue(result.second != null);
+        assertTrue(result.second.length() > 0);
+        assertEquals("PATCH: " + PAYLOAD + EXPECT_STRING, result.second);
+
+        result = mgr.patch(patchUriBlank, "", null, null, PAYLOAD);
+        assertEquals((Integer)200, result.first);
+        assertTrue(result.second != null);
+        assertTrue(result.second.length() > 0);
+        assertEquals("PATCH: " + PAYLOAD + RETURN_STRING, result.second);
     }
 
     @Test
@@ -250,6 +291,18 @@ public class RestTest {
         assertTrue(result.second != null);
         assertTrue(result.second.length() > 0);
         assertEquals("POST: " + PAYLOAD + RETURN_STRING, result.second);
+
+        result = mgr.patch(patchUri, "user", null, null, PAYLOAD);
+        assertEquals((Integer)200, result.first);
+        assertTrue(result.second != null);
+        assertTrue(result.second.length() > 0);
+        assertEquals("PATCH: " + PAYLOAD + EXPECT_STRING, result.second);
+
+        result = mgr.patch(patchUriBlank, "user", null, null, PAYLOAD);
+        assertEquals((Integer)200, result.first);
+        assertTrue(result.second != null);
+        assertTrue(result.second.length() > 0);
+        assertEquals("PATCH: " + PAYLOAD + RETURN_STRING, result.second);
     }
 
     @Test
@@ -267,6 +320,9 @@ public class RestTest {
 
         result = mgr.post(baseUri + "RestTest/PostHello/", null, null, null, MediaType.TEXT_PLAIN, PAYLOAD);
         assertEquals((Integer)404, result.first);
+
+        result = mgr.patch(baseUri + "RestTest/PatchHello/", null, null, null, PAYLOAD);
+        assertEquals((Integer)404, result.first);
     }
 
     @Test
@@ -297,6 +353,13 @@ public class RestTest {
         assertTrue(result.second != null);
         assertTrue(result.second.length() > 0);
         assertEquals("POST: " + PAYLOAD + RETURN_STRING + NAME_PARAM + " aged 90", result.second);
+
+        result = mgr.patch(baseUri + "RestTest/PatchHello/" + NAME_PARAM, null, null,
+            null, PAYLOAD);
+        assertEquals((Integer)200, result.first);
+        assertTrue(result.second != null);
+        assertTrue(result.second.length() > 0);
+        assertEquals("PATCH: " + PAYLOAD + RETURN_STRING + NAME_PARAM + " aged 90", result.second);
     }
 
     @Test
@@ -314,6 +377,9 @@ public class RestTest {
 
         result = mgr.post(baseUri + "NonExistant/URL/", null, null, null, MediaType.TEXT_PLAIN, PAYLOAD);
         assertEquals((Integer)404, result.first);
+
+        result = mgr.patch(baseUri + "NonExistant/URL/", null, null, null, PAYLOAD);
+        assertEquals((Integer)404, result.first);
     }
 
     @Test
@@ -334,6 +400,9 @@ public class RestTest {
 
         result = mgr.post(getUri, null, null, null, MediaType.TEXT_PLAIN, PAYLOAD);
         assertEquals((Integer)405, result.first);
+
+        result = mgr.patch(getUri, null, null, null, PAYLOAD);
+        assertEquals((Integer)405, result.first);
     }
 
     @GET
@@ -387,4 +456,30 @@ public class RestTest {
     public String postBlank( String payload) {
         return "POST: " + payload + RETURN_STRING;
     }
+
+    @Target({ElementType.METHOD})
+    @Retention(RetentionPolicy.RUNTIME)
+    @HttpMethod("PATCH")
+    @Documented
+    public static @interface Patch {
+    }
+
+    @Patch
+    @Path("/PatchHello/{name}")
+    @Consumes(MERGE_PATCH_PLUS_JSON)
+    @Produces(MERGE_PATCH_PLUS_JSON)
+    public String patchIt(
+        String payload,
+        @PathParam("name") String name,
+        @DefaultValue("90") @QueryParam("age") String age) {
+
+        return "PATCH: " + payload + RETURN_STRING + name + " aged " + age;
+    }
+
+    @Patch
+    @Path("/PatchBlank")
+    @Produces(MERGE_PATCH_PLUS_JSON)
+    public String patchBlank( String payload) {
+        return "PATCH: " + payload + RETURN_STRING;
+    }
 }