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  package com.linecorp.centraldogma.common;
18  
19  import static com.linecorp.centraldogma.internal.Util.validateFilePath;
20  import static com.linecorp.centraldogma.internal.Util.validateJsonFilePath;
21  import static java.util.Objects.requireNonNull;
22  
23  import java.util.Objects;
24  
25  import javax.annotation.Nullable;
26  
27  import com.fasterxml.jackson.annotation.JsonCreator;
28  import com.fasterxml.jackson.annotation.JsonProperty;
29  import com.fasterxml.jackson.core.JsonProcessingException;
30  import com.fasterxml.jackson.databind.JsonNode;
31  import com.google.common.base.MoreObjects;
32  
33  import com.linecorp.centraldogma.internal.Jackson;
34  
35  final class DefaultChange<T> implements Change<T> {
36  
37      @JsonCreator
38      static DefaultChange<?> deserialize(@JsonProperty("type") ChangeType type,
39                                          @JsonProperty("path") String path,
40                                          @JsonProperty("content") @Nullable JsonNode content) {
41          requireNonNull(type, "type");
42          final Class<?> contentType = type.contentType();
43          if (contentType == Void.class) {
44              if (content != null && !content.isNull()) {
45                  return rejectIncompatibleContent(content, Void.class);
46              }
47          } else if (type.contentType() == String.class) {
48              if (content == null || !content.isTextual()) {
49                  return rejectIncompatibleContent(content, String.class);
50              }
51          }
52  
53          if (type == ChangeType.REMOVE) {
54              return (DefaultChange<?>) Change.ofRemoval(path);
55          }
56  
57          requireNonNull(content, "content");
58  
59          final Change<?> result;
60          switch (type) {
61              case UPSERT_JSON:
62                  result = Change.ofJsonUpsert(path, content);
63                  break;
64              case UPSERT_TEXT:
65                  result = Change.ofTextUpsert(path, content.asText());
66                  break;
67              case RENAME:
68                  result = Change.ofRename(path, content.asText());
69                  break;
70              case APPLY_JSON_PATCH:
71                  result = Change.ofJsonPatch(path, content);
72                  break;
73              case APPLY_TEXT_PATCH:
74                  result = Change.ofTextPatch(path, content.asText());
75                  break;
76              default:
77                  // Should never reach here
78                  throw new Error();
79          }
80  
81          // Ugly downcast, but otherwise we would have needed to write a custom serializer.
82          return (DefaultChange<?>) result;
83      }
84  
85      private static DefaultChange<?> rejectIncompatibleContent(@Nullable JsonNode content,
86                                                                Class<?> contentType) {
87          throw new IllegalArgumentException("incompatible content: " + content +
88                                             " (expected: " + contentType.getName() + ')');
89      }
90  
91      private final String path;
92      private final ChangeType type;
93      @Nullable
94      private final T content;
95      @Nullable
96      private String contentAsText;
97  
98      DefaultChange(String path, ChangeType type, @Nullable T content) {
99          this.type = requireNonNull(type, "type");
100 
101         if (type.contentType() == JsonNode.class) {
102             validateJsonFilePath(path, "path");
103         } else {
104             validateFilePath(path, "path");
105         }
106 
107         this.path = path;
108         this.content = content;
109     }
110 
111     @Override
112     public String path() {
113         return path;
114     }
115 
116     @Override
117     public ChangeType type() {
118         return type;
119     }
120 
121     @Override
122     public T content() {
123         return content;
124     }
125 
126     @Override
127     @Nullable
128     public String contentAsText() {
129         if (contentAsText != null) {
130             return contentAsText;
131         }
132         if (content == null) {
133             return null;
134         }
135 
136         if (content instanceof CharSequence) {
137             return contentAsText = content.toString();
138         }
139 
140         if (content instanceof JsonNode) {
141             try {
142                 return contentAsText = Jackson.writeValueAsString(content);
143             } catch (JsonProcessingException e) {
144                 // Should never reach here.
145                 throw new Error(e);
146             }
147         }
148 
149         throw new Error();
150     }
151 
152     @Override
153     public int hashCode() {
154         return type.hashCode() * 31 + path.hashCode();
155     }
156 
157     @Override
158     public boolean equals(Object o) {
159         if (this == o) {
160             return true;
161         }
162         if (!(o instanceof Change)) {
163             return false;
164         }
165 
166         final Change<?> that = (Change<?>) o;
167         if (type != that.type()) {
168             return false;
169         }
170 
171         if (!path.equals(that.path())) {
172             return false;
173         }
174 
175         return Objects.equals(content, that.content());
176     }
177 
178     @Override
179     public String toString() {
180         return MoreObjects.toStringHelper(Change.class)
181                           .add("type", type)
182                           .add("path", path)
183                           .add("content", content)
184                           .toString();
185     }
186 }