1
2
3
4
5
6
7
8
9
10
11
12
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
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
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
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
111
112
113 if (createFile) {
114 cacheEntry.doCreate();
115 }
116 } else {
117
118 final Properties props = new Properties();
119
120 if (!canonicalFile.exists()) {
121 cacheEntry = new ValueCacheEntry(props, false, createFile);
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
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 }