View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package org.basepom.mojo.propertyhelper;
16  
17  import static com.google.common.base.Preconditions.checkNotNull;
18  import static com.google.common.base.Preconditions.checkState;
19  import static java.lang.String.format;
20  import static org.basepom.mojo.propertyhelper.IgnoreWarnFailCreate.checkIgnoreWarnFailCreateState;
21  
22  import org.basepom.mojo.propertyhelper.ValueProvider.MapBackedValueAdapter;
23  import org.basepom.mojo.propertyhelper.definitions.FieldDefinition;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.nio.file.Files;
30  import java.util.Map;
31  import java.util.Map.Entry;
32  import java.util.Objects;
33  import java.util.Optional;
34  import java.util.Properties;
35  import java.util.StringJoiner;
36  
37  import com.google.common.annotations.VisibleForTesting;
38  import com.google.common.collect.ForwardingMap;
39  import com.google.common.collect.Maps;
40  import com.google.common.flogger.FluentLogger;
41  
42  public final class ValueCache {
43  
44      private static final FluentLogger LOG = FluentLogger.forEnclosingClass();
45  
46      private final Map<String, String> ephemeralValues = Maps.newHashMap();
47  
48      /**
49       * Cache for values files loaded from disk
50       */
51      private final Map<File, ValueCacheEntry> valueFiles = Maps.newHashMap();
52  
53      @VisibleForTesting
54      public ValueProvider findCurrentValueProvider(final Map<String, String> values, final FieldDefinition<?> definition) {
55          checkNotNull(values, "values is null");
56  
57          final String propertyNameInFile = definition.getPropertyNameInFile();
58          final boolean hasValue = values.containsKey(propertyNameInFile);
59  
60          final boolean createProperty = checkIgnoreWarnFailCreateState(hasValue, definition.getOnMissingFileProperty(),
61              () -> format("property '%s' has value '%s'", propertyNameInFile, values.get(propertyNameInFile)),
62              () -> format("property '%s' has no value defined", propertyNameInFile));
63  
64          if (hasValue) {
65              return new MapBackedValueAdapter(values, propertyNameInFile);
66          } else if (createProperty) {
67              Optional<String> initialValue = definition.getInitialValue();
68              initialValue.ifPresent(value -> values.put(propertyNameInFile, value));
69  
70              return new MapBackedValueAdapter(values, propertyNameInFile);
71          } else {
72              return ValueProvider.NULL_PROVIDER;
73          }
74      }
75  
76      public ValueProvider getValueProvider(final FieldDefinition<?> definition) throws IOException {
77          final Optional<Map<String, String>> values = getValues(definition);
78          if (values.isEmpty()) {
79              final String name = definition.getId();
80              final Optional<String> initialValue = definition.getInitialValue();
81              initialValue.ifPresent(s -> ephemeralValues.put(name, s));
82  
83              return new MapBackedValueAdapter(ephemeralValues, name);
84          } else {
85              return findCurrentValueProvider(values.get(), definition);
86          }
87      }
88  
89      @VisibleForTesting
90      Optional<Map<String, String>> getValues(final FieldDefinition<?> definition) throws IOException {
91          final Optional<File> definitionFile = definition.getPropertyFile();
92  
93          // Ephemeral, so return null.
94          if (definitionFile.isEmpty()) {
95              return Optional.empty();
96          }
97  
98          ValueCacheEntry cacheEntry;
99          final File canonicalFile = definitionFile.get().getCanonicalFile();
100         final String canonicalPath = definitionFile.get().getCanonicalPath();
101 
102         // Throws an exception if the file must exist and does not.
103         final boolean createFile = checkIgnoreWarnFailCreateState(canonicalFile.exists(), definition.getOnMissingFile(),
104             () -> format("property file '%s' exists", canonicalPath),
105             () -> format("property file '%s' does not exist!", canonicalPath));
106 
107         cacheEntry = valueFiles.get(canonicalFile);
108 
109         if (cacheEntry != null) {
110             // If there is a cache hit, something either has loaded the file
111             // or another property has already put in a creation order.
112             // Make sure that if this number has a creation order it is obeyed.
113             if (createFile) {
114                 cacheEntry.doCreate();
115             }
116         } else {
117             // Try loading or creating properties.
118             final Properties props = new Properties();
119 
120             if (!canonicalFile.exists()) {
121                 cacheEntry = new ValueCacheEntry(props, false, createFile); // does not exist
122                 valueFiles.put(canonicalFile, cacheEntry);
123             } else {
124                 if (canonicalFile.isFile() && canonicalFile.canRead()) {
125                     try (InputStream stream = Files.newInputStream(canonicalFile.toPath())) {
126                         props.load(stream);
127                         cacheEntry = new ValueCacheEntry(props, true, createFile);
128                         valueFiles.put(canonicalFile, cacheEntry);
129                     }
130                 } else {
131                     throw new IllegalStateException(
132                         format("Can not load %s, not a file!", definitionFile.get().getCanonicalPath()));
133                 }
134             }
135         }
136 
137         return Optional.of(cacheEntry.getValues());
138     }
139 
140     public void persist()
141         throws IOException {
142         for (final Entry<File, ValueCacheEntry> entries : valueFiles.entrySet()) {
143             final ValueCacheEntry entry = entries.getValue();
144             if (!entry.isDirty()) {
145                 continue;
146             }
147             final File file = entries.getKey();
148             if (entry.isExists() || entry.isCreate()) {
149                 checkNotNull(file, "no file defined, can not persist!");
150                 final File oldFile = new File(file.getCanonicalPath() + ".bak");
151 
152                 if (entry.isExists()) {
153                     checkState(file.exists(), "'%s' should exist!", file.getCanonicalPath());
154                     // unlink an old file if necessary
155                     if (oldFile.exists()) {
156                         checkState(oldFile.delete(), "Could not delete '%s'", file.getCanonicalPath());
157                     }
158                 }
159 
160                 final File folder = file.getParentFile();
161                 if (!folder.exists()) {
162                     checkState(folder.mkdirs(), "Could not create folder '%s'", folder.getCanonicalPath());
163                 }
164 
165                 final File newFile = new File(file.getCanonicalPath() + ".new");
166                 try (OutputStream stream = Files.newOutputStream(newFile.toPath())) {
167                     entry.store(stream, "created by property-helper-maven-plugin");
168                 }
169 
170                 if (file.exists()) {
171                     if (!file.renameTo(oldFile)) {
172                         LOG.atWarning().log("Could not rename '%s' to '%s'!", file, oldFile);
173                     }
174                 }
175 
176                 if (!file.exists()) {
177                     if (!newFile.renameTo(file)) {
178                         LOG.atWarning().log("Could not rename '%s' to '%s'!", newFile, file);
179                     }
180                 }
181             }
182         }
183     }
184 
185     public static class ValueCacheEntry {
186 
187         private final Map<String, String> values = Maps.newHashMap();
188 
189         private final boolean exists;
190 
191         private boolean create;
192 
193         private boolean dirty = false;
194 
195         ValueCacheEntry(final Properties props,
196             final boolean exists,
197             final boolean create) {
198             checkNotNull(props, "props is null");
199 
200             values.putAll(Maps.fromProperties(props));
201 
202             this.exists = exists;
203             this.create = create;
204         }
205 
206         public void store(final OutputStream out, final String comment)
207             throws IOException {
208             final Properties p = new Properties();
209             for (Entry<String, String> entry : values.entrySet()) {
210                 p.setProperty(entry.getKey(), entry.getValue());
211             }
212             p.store(out, comment);
213         }
214 
215         public boolean isDirty() {
216             return dirty;
217         }
218 
219         public void dirty() {
220             this.dirty = true;
221         }
222 
223         public Map<String, String> getValues() {
224             return new ForwardingMap<>() {
225                 @Override
226                 protected Map<String, String> delegate() {
227                     return values;
228                 }
229 
230                 @Override
231                 public String remove(Object object) {
232                     dirty();
233                     return super.remove(object);
234                 }
235 
236                 @Override
237                 public void clear() {
238                     dirty();
239                     super.clear();
240                 }
241 
242                 @Override
243                 public String put(String key, String value) {
244                     final String oldValue = super.put(key, value);
245                     if (!Objects.equals(value, oldValue)) {
246                         dirty();
247                     }
248                     return oldValue;
249                 }
250 
251                 @Override
252                 public void putAll(Map<? extends String, ? extends String> map) {
253                     dirty();
254                     super.putAll(map);
255                 }
256             };
257         }
258 
259         public boolean isExists() {
260             return exists;
261         }
262 
263         public boolean isCreate() {
264             return create;
265         }
266 
267         public void doCreate() {
268             this.create = true;
269             dirty();
270         }
271 
272         @Override
273         public String toString() {
274             return new StringJoiner(", ", ValueCacheEntry.class.getSimpleName() + "[", "]")
275                 .add("values=" + values)
276                 .add("exists=" + exists)
277                 .add("create=" + create)
278                 .add("dirty=" + dirty)
279                 .toString();
280         }
281 
282         @Override
283         public boolean equals(Object o) {
284             if (this == o) {
285                 return true;
286             }
287             if (o == null || getClass() != o.getClass()) {
288                 return false;
289             }
290             ValueCacheEntry that = (ValueCacheEntry) o;
291             return exists == that.exists && create == that.create && dirty == that.dirty
292                 && Objects.equals(values, that.values);
293         }
294 
295         @Override
296         public int hashCode() {
297             return Objects.hash(values, exists, create, dirty);
298         }
299     }
300 }