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.google.common.base.Preconditions.checkArgument;
20  import static java.util.Objects.requireNonNull;
21  
22  import java.util.Objects;
23  import java.util.function.Consumer;
24  
25  import javax.annotation.Nullable;
26  
27  import com.fasterxml.jackson.core.JsonParseException;
28  import com.fasterxml.jackson.databind.JsonNode;
29  import com.google.common.base.MoreObjects;
30  
31  import com.linecorp.centraldogma.internal.Jackson;
32  
33  /**
34   * A file or a directory in a repository.
35   *
36   * @param <T> the content type. {@link JsonNode} if JSON. {@link String} if text.
37   */
38  public final class Entry<T> implements ContentHolder<T> {
39  
40      /**
41       * Returns a newly-created {@link Entry} of a directory.
42       *
43       * @param revision the revision of the directory
44       * @param path the path of the directory
45       */
46      public static Entry<Void> ofDirectory(Revision revision, String path) {
47          return new Entry<>(revision, path, EntryType.DIRECTORY, null);
48      }
49  
50      /**
51       * Returns a newly-created {@link Entry} of a JSON file.
52       *
53       * @param revision the revision of the JSON file
54       * @param path the path of the JSON file
55       * @param content the content of the JSON file
56       */
57      public static Entry<JsonNode> ofJson(Revision revision, String path, JsonNode content) {
58          return new Entry<>(revision, path, EntryType.JSON, content);
59      }
60  
61      /**
62       * Returns a newly-created {@link Entry} of a JSON file.
63       *
64       * @param revision the revision of the JSON file
65       * @param path the path of the JSON file
66       * @param content the content of the JSON file
67       *
68       * @throws JsonParseException if the {@code content} is not a valid JSON
69       */
70      public static Entry<JsonNode> ofJson(Revision revision, String path, String content)
71              throws JsonParseException {
72          return ofJson(revision, path, Jackson.readTree(content));
73      }
74  
75      /**
76       * Returns a newly-created {@link Entry} of a text file.
77       *
78       * @param revision the revision of the text file
79       * @param path the path of the text file
80       * @param content the content of the text file
81       */
82      public static Entry<String> ofText(Revision revision, String path, String content) {
83          return new Entry<>(revision, path, EntryType.TEXT, content);
84      }
85  
86      /**
87       * Returns a newly-created {@link Entry}.
88       *
89       * @param revision the revision of the {@link Entry}
90       * @param path the path of the {@link Entry}
91       * @param content the content of the {@link Entry}
92       * @param type the type of the {@link Entry}
93       * @param <T> the content type. {@link JsonNode} if JSON. {@link String} if text.
94       */
95      public static <T> Entry<T> of(Revision revision, String path, EntryType type, @Nullable T content) {
96          return new Entry<>(revision, path, type, content);
97      }
98  
99      private final Revision revision;
100     private final String path;
101     @Nullable
102     private final T content;
103     private final EntryType type;
104     @Nullable
105     private String contentAsText;
106     @Nullable
107     private String contentAsPrettyText;
108 
109     /**
110      * Creates a new instance.
111      *
112      * @param revision the revision of the entry
113      * @param path the path of the entry
114      * @param type the type of given {@code content}
115      * @param content an object of given type {@code T}
116      */
117     private Entry(Revision revision, String path, EntryType type, @Nullable T content) {
118         requireNonNull(revision, "revision");
119         checkArgument(!revision.isRelative(), "revision: %s (expected: absolute revision)", revision);
120         this.revision = revision;
121         this.path = requireNonNull(path, "path");
122         this.type = requireNonNull(type, "type");
123 
124         final Class<?> entryContentType = type.type();
125 
126         if (entryContentType == Void.class) {
127             checkArgument(content == null, "content: %s (expected: null)", content);
128             this.content = null;
129         } else {
130             @SuppressWarnings("unchecked")
131             final T castContent = (T) entryContentType.cast(requireNonNull(content, "content"));
132             this.content = castContent;
133         }
134     }
135 
136     /**
137      * Returns the revision of this {@link Entry}.
138      */
139     public Revision revision() {
140         return revision;
141     }
142 
143     /**
144      * Returns the path of this {@link Entry}.
145      */
146     public String path() {
147         return path;
148     }
149 
150     /**
151      * Returns if this {@link Entry} has content, which is always {@code true} if it's not a directory.
152      */
153     public boolean hasContent() {
154         return content != null;
155     }
156 
157     /**
158      * If this {@link Entry} has content, invoke the specified {@link Consumer} with the content.
159      */
160     public void ifHasContent(Consumer<? super T> consumer) {
161         requireNonNull(consumer, "consumer");
162         if (content != null) {
163             consumer.accept(content);
164         }
165     }
166 
167     @Override
168     public EntryType type() {
169         return type;
170     }
171 
172     @Override
173     public T content() {
174         if (content == null) {
175             throw new EntryNoContentException(type, revision, path);
176         }
177         return content;
178     }
179 
180     @Override
181     public String contentAsText() {
182         if (contentAsText == null) {
183             contentAsText = ContentHolder.super.contentAsText();
184         }
185         return contentAsText;
186     }
187 
188     @Override
189     public String contentAsPrettyText() {
190         if (contentAsPrettyText == null) {
191             contentAsPrettyText = ContentHolder.super.contentAsPrettyText();
192         }
193         return contentAsPrettyText;
194     }
195 
196     @Override
197     public int hashCode() {
198         return (revision.hashCode() * 31 + type.hashCode()) * 31 + path.hashCode();
199     }
200 
201     @Override
202     public boolean equals(Object o) {
203         if (this == o) {
204             return true;
205         }
206         if (!(o instanceof Entry)) {
207             return false;
208         }
209 
210         @SuppressWarnings("unchecked")
211         final Entry<T> that = (Entry<T>) o;
212 
213         return type == that.type && revision.equals(that.revision) && path.equals(that.path) &&
214                Objects.equals(content, that.content);
215     }
216 
217     @Override
218     public String toString() {
219         return MoreObjects.toStringHelper(this).omitNullValues()
220                           .add("revision", revision.text())
221                           .add("path", path)
222                           .add("type", type)
223                           .add("content", hasContent() ? contentAsText() : null)
224                           .toString();
225     }
226 }