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