001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.config.store; 018 019import static java.util.Collections.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022 023import java.io.*; 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.config.format.*; 028import org.apache.juneau.config.internal.*; 029import org.apache.juneau.marshall.*; 030 031/** 032 * Represents a storage location for configuration files. 033 * 034 * <p> 035 * Content stores require two methods to be implemented: 036 * <ul class='javatree'> 037 * <li class='jm'>{@link #read(String)} - Retrieve a config file. 038 * <li class='jm'>{@link #write(String,String,String)} - Store a config file. 039 * </ul> 040 * 041 * <h5 class='section'>Notes:</h5><ul> 042 * <li class='note'>This class is thread safe and reusable. 043 * </ul> 044*/ 045@SuppressWarnings({ 046 "rawtypes", // The protected ConfigStore(Builder) constructor takes a raw Builder so subclasses can pass their concrete Builder<SELF> without naming the self-type 047 "resource" // ConfigStore manages Closeable resources 048}) 049public abstract class ConfigStore extends Context implements Closeable { 050 051 /** 052 * Builder class. 053 */ 054 @SuppressWarnings({ 055 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 056 }) 057 public abstract static class Builder<SELF extends Builder<SELF>> extends Context.Builder<SELF> { 058 059 /** 060 * Constructor, default settings. 061 */ 062 protected Builder() {} 063 064 /** 065 * Copy constructor. 066 * 067 * @param copyFrom The builder to copy from. 068 */ 069 protected Builder(Builder<?> copyFrom) { 070 super(copyFrom); 071 } 072 073 /** 074 * Copy constructor. 075 * 076 * @param copyFrom The bean to copy from. 077 */ 078 protected Builder(ConfigStore copyFrom) { 079 super(copyFrom); 080 } 081 082 } 083 084 private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>(); 085 private final ConcurrentHashMap<String,Set<ConfigStoreListener>> listeners = new ConcurrentHashMap<>(); 086 087 // Tracks the chain of configs currently being constructed on this thread so that a circular import 088 // (a config that directly or indirectly imports itself) can be detected and reported cleanly instead 089 // of recursing infinitely through getMap()->new ConfigMap()->load()->getMap() until the stack overflows. 090 private static final ThreadLocal<Set<String>> LOADING = ThreadLocal.withInitial(LinkedHashSet::new); 091 092 /** 093 * Constructor. 094 * 095 * @param builder The builder for this object. 096 * <br>Cannot be <jk>null</jk>. 097 */ 098 protected ConfigStore(Builder builder) { 099 super(builder); 100 } 101 102 /** 103 * Checks whether the configuration with the specified name exists in this store. 104 * 105 * @param name The config name. 106 * @return <jk>true</jk> if the configuration with the specified name exists in this store. 107 */ 108 public abstract boolean exists(String name); 109 110 /** 111 * Returns a map file containing the parsed contents of a configuration. 112 * 113 * @param name The configuration name. 114 * @return 115 * The parsed configuration. 116 * <br>Never <jk>null</jk>. 117 * @throws IOException Thrown by underlying stream. 118 */ 119 public synchronized ConfigMap getMap(String name) throws IOException { 120 return getMap(name, IniConfigFormat.INSTANCE); 121 } 122 123 /** 124 * Returns a map file containing the parsed contents of a configuration. 125 * 126 * @param name The configuration name. 127 * @param format 128 * The configuration format. 129 * <br>Can be <jk>null</jk> (defaults to {@link IniConfigFormat}). 130 * @return The parsed configuration. 131 * @throws IOException Thrown by underlying stream. 132 */ 133 @SuppressWarnings({ 134 "javabugs:S2259" // False positive: 'format2' is never null; a null 'format' defaults to the static final IniConfigFormat.INSTANCE. 135 }) 136 public synchronized ConfigMap getMap(String name, ConfigFormat format) throws IOException { 137 name = resolveName(name); 138 var format2 = format == null ? IniConfigFormat.INSTANCE : format; 139 var key = format2.id() + ":" + name; 140 var cm = configMaps.get(key); 141 if (nn(cm)) 142 return cm; 143 144 // Detect circular imports before recursing into ConfigMap construction. 145 var loading = LOADING.get(); 146 if (! loading.add(key)) 147 throw new ConfigException("Import loop detected in configuration: %s", importChain(loading, name)); 148 try { 149 cm = new ConfigMap(this, name, format2); 150 } finally { 151 loading.remove(key); 152 if (loading.isEmpty()) 153 LOADING.remove(); 154 } 155 156 var cm2 = configMaps.putIfAbsent(key, cm); 157 if (nn(cm2)) 158 return cm2; 159 register(name, cm); 160 return cm; 161 } 162 163 // Renders the chain of configs currently being loaded (e.g. "B -> A2 -> A1 -> A2") for a circular-import error message. 164 // The entries in the loading set are cache keys of the form "<formatId>:<name>", so the format prefix is stripped. 165 private static String importChain(Set<String> loading, String reentry) { 166 var sb = new StringBuilder(); 167 for (var key : loading) 168 sb.append(key.substring(key.indexOf(':') + 1)).append(" -> "); 169 return sb.append(reentry).toString(); 170 } 171 172 /** 173 * Returns the contents of the configuration file. 174 * 175 * @param name The config file name. 176 * @return 177 * The contents of the configuration file. 178 * <br>A blank string if the config does not exist. 179 * <br>Never <jk>null</jk>. 180 * @throws IOException Thrown by underlying stream. 181 */ 182 public abstract String read(String name) throws IOException; 183 184 /** 185 * Registers a new listener on this store. 186 * 187 * @param name 188 * The configuration name to listen for. 189 * <br>Cannot be <jk>null</jk>. 190 * @param l The new listener. 191 * @return This object. 192 */ 193 public synchronized ConfigStore register(String name, ConfigStoreListener l) { 194 name = resolveName(name); 195 var s = listeners.computeIfAbsent(name, k -> synced(newSetFromMap(new IdentityHashMap<>()))); 196 s.add(l); 197 return this; 198 } 199 200 /** 201 * Unregisters a listener from this store. 202 * 203 * @param name 204 * The configuration name to listen for. 205 * <br>Cannot be <jk>null</jk>. 206 * @param l The listener to unregister. 207 * @return This object. 208 */ 209 public synchronized ConfigStore unregister(String name, ConfigStoreListener l) { 210 name = resolveName(name); 211 var s = listeners.get(name); 212 if (nn(s)) 213 s.remove(l); 214 return this; 215 } 216 217 /** 218 * Called when the physical contents of a config file have changed. 219 * 220 * <p> 221 * Triggers calls to {@link ConfigStoreListener#onChange(String)} on all registered listeners. 222 * 223 * @param name 224 * The config name (e.g. the filename without the extension). 225 * <br>Cannot be <jk>null</jk>. 226 * @param contents The new contents. 227 * @return This object. 228 */ 229 public synchronized ConfigStore update(String name, String contents) { 230 name = resolveName(name); 231 var s = listeners.get(name); 232 if (nn(s)) 233 listeners.get(name).forEach(x -> x.onChange(contents)); 234 return this; 235 } 236 237 /** 238 * Convenience method for updating the contents of a file with lines. 239 * 240 * @param name 241 * The config name (e.g. the filename without the extension). 242 * <br>Cannot be <jk>null</jk>. 243 * @param contentLines The new contents. 244 * @return This object. 245 */ 246 public synchronized ConfigStore update(String name, String...contentLines) { 247 name = resolveName(name); 248 var sb = new StringBuilder(); 249 for (var l : contentLines) 250 sb.append(l).append('\n'); 251 return update(name, sb.toString()); 252 } 253 254 /** 255 * Saves the contents of the configuration file if the underlying storage hasn't been modified. 256 * 257 * @param name The config file name. 258 * @param expectedContents The expected contents of the file. 259 * @param newContents The new contents. 260 * @return 261 * If <jk>null</jk>, then we successfully stored the contents of the file. 262 * <br>Otherwise the contents of the file have changed and we return the new contents of the file. 263 * @throws IOException Thrown by underlying stream. 264 */ 265 public abstract String write(String name, String expectedContents, String newContents) throws IOException; 266 267 /** 268 * Returns the contents that listeners will be notified with after the specified contents are written. 269 * 270 * <p> 271 * Stores that notify listeners with text other than what was written (e.g. after merging in overlays) should 272 * override this method. 273 * 274 * <p> 275 * This is called before the write, so any processing that fails (e.g. merging unparseable written text) fails 276 * before anything is persisted. 277 * 278 * @param name The config name. 279 * @param written The contents being written. 280 * @return The contents that listeners will receive. The default implementation returns <c>written</c>. 281 * @throws IOException Thrown by underlying stream. 282 */ 283 public String getNotifiedContents(String name, String written) throws IOException { 284 return written; 285 } 286 287 /** 288 * Subclasses can override this method to convert config names to internal forms. 289 * 290 * <p> 291 * For example, the {@link FileStore} class can take in both <js>"MyConfig"</js> and <js>"MyConfig.cfg"</js> 292 * as names that both resolve to <js>"MyConfig.cfg"</js>. 293 * 294 * @param name The name to resolve. 295 * @return The resolved name. 296 */ 297 protected String resolveName(String name) { 298 return name; 299 } 300}