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}