001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package org.basepom.mojo.propertyhelper;
016
017import static com.google.common.base.Preconditions.checkNotNull;
018import static com.google.common.base.Preconditions.checkState;
019import static java.lang.String.format;
020import static org.basepom.mojo.propertyhelper.IgnoreWarnFailCreate.checkIgnoreWarnFailCreateState;
021
022import org.basepom.mojo.propertyhelper.ValueProvider.MapBackedValueAdapter;
023import org.basepom.mojo.propertyhelper.definitions.FieldDefinition;
024
025import java.io.File;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.nio.file.Files;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Objects;
033import java.util.Optional;
034import java.util.Properties;
035import java.util.StringJoiner;
036
037import com.google.common.annotations.VisibleForTesting;
038import com.google.common.collect.ForwardingMap;
039import com.google.common.collect.Maps;
040import com.google.common.flogger.FluentLogger;
041
042public final class ValueCache {
043
044    private static final FluentLogger LOG = FluentLogger.forEnclosingClass();
045
046    private final Map<String, String> ephemeralValues = Maps.newHashMap();
047
048    /**
049     * Cache for values files loaded from disk
050     */
051    private final Map<File, ValueCacheEntry> valueFiles = Maps.newHashMap();
052
053    @VisibleForTesting
054    public ValueProvider findCurrentValueProvider(final Map<String, String> values, final FieldDefinition<?> definition) {
055        checkNotNull(values, "values is null");
056
057        final String propertyNameInFile = definition.getPropertyNameInFile();
058        final boolean hasValue = values.containsKey(propertyNameInFile);
059
060        final boolean createProperty = checkIgnoreWarnFailCreateState(hasValue, definition.getOnMissingFileProperty(),
061            () -> format("property '%s' has value '%s'", propertyNameInFile, values.get(propertyNameInFile)),
062            () -> format("property '%s' has no value defined", propertyNameInFile));
063
064        if (hasValue) {
065            return new MapBackedValueAdapter(values, propertyNameInFile);
066        } else if (createProperty) {
067            Optional<String> initialValue = definition.getInitialValue();
068            initialValue.ifPresent(value -> values.put(propertyNameInFile, value));
069
070            return new MapBackedValueAdapter(values, propertyNameInFile);
071        } else {
072            return ValueProvider.NULL_PROVIDER;
073        }
074    }
075
076    public ValueProvider getValueProvider(final FieldDefinition<?> definition) throws IOException {
077        final Optional<Map<String, String>> values = getValues(definition);
078        if (values.isEmpty()) {
079            final String name = definition.getId();
080            final Optional<String> initialValue = definition.getInitialValue();
081            initialValue.ifPresent(s -> ephemeralValues.put(name, s));
082
083            return new MapBackedValueAdapter(ephemeralValues, name);
084        } else {
085            return findCurrentValueProvider(values.get(), definition);
086        }
087    }
088
089    @VisibleForTesting
090    Optional<Map<String, String>> getValues(final FieldDefinition<?> definition) throws IOException {
091        final Optional<File> definitionFile = definition.getPropertyFile();
092
093        // Ephemeral, so return null.
094        if (definitionFile.isEmpty()) {
095            return Optional.empty();
096        }
097
098        ValueCacheEntry cacheEntry;
099        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}