1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  /*
17   * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
18   *
19   * This software is dual-licensed under:
20   *
21   * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
22   *   later version;
23   * - the Apache Software License (ASL) version 2.0.
24   *
25   * The text of this file and of both licenses is available at the root of this
26   * project or, if you have the jar distribution, in directory META-INF/, under
27   * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
28   *
29   * Direct link to the sources:
30   *
31   * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
32   * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
33   */
34  
35  package com.linecorp.centraldogma.internal.jsonpatch;
36  
37  import static com.fasterxml.jackson.annotation.JsonSubTypes.Type;
38  import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
39  import static java.util.Objects.requireNonNull;
40  
41  import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
42  import com.fasterxml.jackson.annotation.JsonSubTypes;
43  import com.fasterxml.jackson.annotation.JsonTypeInfo;
44  import com.fasterxml.jackson.core.JsonPointer;
45  import com.fasterxml.jackson.databind.JsonNode;
46  import com.fasterxml.jackson.databind.JsonSerializable;
47  import com.fasterxml.jackson.databind.node.JsonNodeFactory;
48  
49  import com.linecorp.centraldogma.internal.Jackson;
50  
51  /**
52   * Base abstract class for one patch operation.
53   *
54   * <p>Two more abstract classes extend this one according to the arguments of
55   * the operation:</p>
56   *
57   * <ul>
58   *     <li>{@link DualPathOperation} for operations taking a second pointer as
59   *     an argument ({@code copy} and {@code move});</li>
60   *     <li>{@link PathValueOperation} for operations taking a value as an
61   *     argument ({@code add}, {@code replace} and {@code test}).</li>
62   * </ul>
63   */
64  @JsonTypeInfo(use = Id.NAME, property = "op")
65  @JsonSubTypes({
66          @Type(name = "add", value = AddOperation.class),
67          @Type(name = "copy", value = CopyOperation.class),
68          @Type(name = "move", value = MoveOperation.class),
69          @Type(name = "remove", value = RemoveOperation.class),
70          @Type(name = "removeIfExists", value = RemoveIfExistsOperation.class),
71          @Type(name = "replace", value = ReplaceOperation.class),
72          @Type(name = "safeReplace", value = SafeReplaceOperation.class),
73          @Type(name = "test", value = TestOperation.class),
74          @Type(name = "testAbsence", value = TestAbsenceOperation.class)
75  })
76  @JsonIgnoreProperties(ignoreUnknown = true)
77  public abstract class JsonPatchOperation implements JsonSerializable {
78  
79      /**
80       * Converts {@link JsonPatchOperation}s to an array of {@link JsonNode}.
81       */
82      public static JsonNode asJsonArray(JsonPatchOperation... jsonPatchOperations) {
83          requireNonNull(jsonPatchOperations, "jsonPatchOperations");
84          return Jackson.valueToTree(jsonPatchOperations);
85      }
86  
87      final String op;
88  
89      /*
90       * Note: no need for a custom deserializer, Jackson will try and find a
91       * constructor with a single string argument and use it.
92       *
93       * However, we need to serialize using .toString().
94       */
95      final JsonPointer path;
96  
97      /**
98       * Creates a new instance.
99       *
100      * @param op the operation name
101      * @param path the JSON Pointer for this operation
102      */
103     JsonPatchOperation(final String op, final JsonPointer path) {
104         this.op = op;
105         this.path = path;
106     }
107 
108     public JsonPointer path() {
109         return path;
110     }
111 
112     /**
113      * Applies this operation to a JSON value.
114      *
115      * @param node the value to patch
116      * @return the patched value
117      * @throws JsonPatchException operation failed to apply to this value
118      */
119     abstract JsonNode apply(JsonNode node);
120 
121     @Override
122     public abstract String toString();
123 
124     /**
125      * Converts this {@link JsonPatchOperation} to a {@link JsonNode}.
126      */
127     public JsonNode toJsonNode() {
128         return JsonNodeFactory.instance.arrayNode().add(Jackson.valueToTree(this));
129     }
130 
131     JsonNode ensureExistence(JsonNode node) {
132         final JsonNode found = node.at(path);
133         if (found.isMissingNode()) {
134             throw new JsonPatchException("non-existent path: " + path);
135         }
136         return found;
137     }
138 
139     static JsonNode ensureSourceParent(JsonNode node, JsonPointer path) {
140         return ensureParent(node, path, "source");
141     }
142 
143     static JsonNode ensureTargetParent(JsonNode node, JsonPointer path) {
144         return ensureParent(node, path, "target");
145     }
146 
147     private static JsonNode ensureParent(JsonNode node, JsonPointer path, String typeName) {
148         /*
149          * Check the parent node: it must exist and be a container (ie an array
150          * or an object) for the add operation to work.
151          */
152         final JsonPointer parentPath = path.head();
153         final JsonNode parentNode = node.at(parentPath);
154         if (parentNode.isMissingNode()) {
155             throw new JsonPatchException("non-existent " + typeName + " parent: " + parentPath);
156         }
157         if (!parentNode.isContainerNode()) {
158             throw new JsonPatchException(typeName + " parent is not a container: " + parentPath +
159                                          " (" + parentNode.getNodeType() + ')');
160         }
161         return parentNode;
162     }
163 }