ValueCache.java
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.basepom.mojo.propertyhelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static org.basepom.mojo.propertyhelper.IgnoreWarnFailCreate.checkIgnoreWarnFailCreateState;
import org.basepom.mojo.propertyhelper.ValueProvider.MapBackedValueAdapter;
import org.basepom.mojo.propertyhelper.definitions.FieldDefinition;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.StringJoiner;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
public final class ValueCache {
private static final FluentLogger LOG = FluentLogger.forEnclosingClass();
private final Map<String, String> ephemeralValues = Maps.newHashMap();
/**
* Cache for values files loaded from disk
*/
private final Map<File, ValueCacheEntry> valueFiles = Maps.newHashMap();
@VisibleForTesting
public ValueProvider findCurrentValueProvider(final Map<String, String> values, final FieldDefinition<?> definition) {
checkNotNull(values, "values is null");
final String propertyNameInFile = definition.getPropertyNameInFile();
final boolean hasValue = values.containsKey(propertyNameInFile);
final boolean createProperty = checkIgnoreWarnFailCreateState(hasValue, definition.getOnMissingFileProperty(),
() -> format("property '%s' has value '%s'", propertyNameInFile, values.get(propertyNameInFile)),
() -> format("property '%s' has no value defined", propertyNameInFile));
if (hasValue) {
return new MapBackedValueAdapter(values, propertyNameInFile);
} else if (createProperty) {
Optional<String> initialValue = definition.getInitialValue();
initialValue.ifPresent(value -> values.put(propertyNameInFile, value));
return new MapBackedValueAdapter(values, propertyNameInFile);
} else {
return ValueProvider.NULL_PROVIDER;
}
}
public ValueProvider getValueProvider(final FieldDefinition<?> definition) throws IOException {
final Optional<Map<String, String>> values = getValues(definition);
if (values.isEmpty()) {
final String name = definition.getId();
final Optional<String> initialValue = definition.getInitialValue();
initialValue.ifPresent(s -> ephemeralValues.put(name, s));
return new MapBackedValueAdapter(ephemeralValues, name);
} else {
return findCurrentValueProvider(values.get(), definition);
}
}
@VisibleForTesting
Optional<Map<String, String>> getValues(final FieldDefinition<?> definition) throws IOException {
final Optional<File> definitionFile = definition.getPropertyFile();
// Ephemeral, so return null.
if (definitionFile.isEmpty()) {
return Optional.empty();
}
ValueCacheEntry cacheEntry;
final File canonicalFile = definitionFile.get().getCanonicalFile();
final String canonicalPath = definitionFile.get().getCanonicalPath();
// Throws an exception if the file must exist and does not.
final boolean createFile = checkIgnoreWarnFailCreateState(canonicalFile.exists(), definition.getOnMissingFile(),
() -> format("property file '%s' exists", canonicalPath),
() -> format("property file '%s' does not exist!", canonicalPath));
cacheEntry = valueFiles.get(canonicalFile);
if (cacheEntry != null) {
// If there is a cache hit, something either has loaded the file
// or another property has already put in a creation order.
// Make sure that if this number has a creation order it is obeyed.
if (createFile) {
cacheEntry.doCreate();
}
} else {
// Try loading or creating properties.
final Properties props = new Properties();
if (!canonicalFile.exists()) {
cacheEntry = new ValueCacheEntry(props, false, createFile); // does not exist
valueFiles.put(canonicalFile, cacheEntry);
} else {
if (canonicalFile.isFile() && canonicalFile.canRead()) {
try (InputStream stream = Files.newInputStream(canonicalFile.toPath())) {
props.load(stream);
cacheEntry = new ValueCacheEntry(props, true, createFile);
valueFiles.put(canonicalFile, cacheEntry);
}
} else {
throw new IllegalStateException(
format("Can not load %s, not a file!", definitionFile.get().getCanonicalPath()));
}
}
}
return Optional.of(cacheEntry.getValues());
}
public void persist()
throws IOException {
for (final Entry<File, ValueCacheEntry> entries : valueFiles.entrySet()) {
final ValueCacheEntry entry = entries.getValue();
if (!entry.isDirty()) {
continue;
}
final File file = entries.getKey();
if (entry.isExists() || entry.isCreate()) {
checkNotNull(file, "no file defined, can not persist!");
final File oldFile = new File(file.getCanonicalPath() + ".bak");
if (entry.isExists()) {
checkState(file.exists(), "'%s' should exist!", file.getCanonicalPath());
// unlink an old file if necessary
if (oldFile.exists()) {
checkState(oldFile.delete(), "Could not delete '%s'", file.getCanonicalPath());
}
}
final File folder = file.getParentFile();
if (!folder.exists()) {
checkState(folder.mkdirs(), "Could not create folder '%s'", folder.getCanonicalPath());
}
final File newFile = new File(file.getCanonicalPath() + ".new");
try (OutputStream stream = Files.newOutputStream(newFile.toPath())) {
entry.store(stream, "created by property-helper-maven-plugin");
}
if (file.exists()) {
if (!file.renameTo(oldFile)) {
LOG.atWarning().log("Could not rename '%s' to '%s'!", file, oldFile);
}
}
if (!file.exists()) {
if (!newFile.renameTo(file)) {
LOG.atWarning().log("Could not rename '%s' to '%s'!", newFile, file);
}
}
}
}
}
public static class ValueCacheEntry {
private final Map<String, String> values = Maps.newHashMap();
private final boolean exists;
private boolean create;
private boolean dirty = false;
ValueCacheEntry(final Properties props,
final boolean exists,
final boolean create) {
checkNotNull(props, "props is null");
values.putAll(Maps.fromProperties(props));
this.exists = exists;
this.create = create;
}
public void store(final OutputStream out, final String comment)
throws IOException {
final Properties p = new Properties();
for (Entry<String, String> entry : values.entrySet()) {
p.setProperty(entry.getKey(), entry.getValue());
}
p.store(out, comment);
}
public boolean isDirty() {
return dirty;
}
public void dirty() {
this.dirty = true;
}
public Map<String, String> getValues() {
return new ForwardingMap<>() {
@Override
protected Map<String, String> delegate() {
return values;
}
@Override
public String remove(Object object) {
dirty();
return super.remove(object);
}
@Override
public void clear() {
dirty();
super.clear();
}
@Override
public String put(String key, String value) {
final String oldValue = super.put(key, value);
if (!Objects.equals(value, oldValue)) {
dirty();
}
return oldValue;
}
@Override
public void putAll(Map<? extends String, ? extends String> map) {
dirty();
super.putAll(map);
}
};
}
public boolean isExists() {
return exists;
}
public boolean isCreate() {
return create;
}
public void doCreate() {
this.create = true;
dirty();
}
@Override
public String toString() {
return new StringJoiner(", ", ValueCacheEntry.class.getSimpleName() + "[", "]")
.add("values=" + values)
.add("exists=" + exists)
.add("create=" + create)
.add("dirty=" + dirty)
.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ValueCacheEntry that = (ValueCacheEntry) o;
return exists == that.exists && create == that.create && dirty == that.dirty
&& Objects.equals(values, that.values);
}
@Override
public int hashCode() {
return Objects.hash(values, exists, create, dirty);
}
}
}