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.internal.CollectionUtils.*; 021 022import java.io.*; 023import java.lang.annotation.*; 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.*; 028import org.apache.juneau.config.internal.*; 029import org.apache.juneau.internal.*; 030import org.apache.juneau.utils.*; 031 032/** 033 * Represents a storage location for configuration files. 034 * 035 * <p> 036 * Content stores require two methods to be implemented: 037 * <ul class='javatree'> 038 * <li class='jm'>{@link #read(String)} - Retrieve a config file. 039 * <li class='jm'>{@link #write(String,String,String)} - ConfigStore a config file. 040 * </ul> 041 * 042 * <h5 class='section'>Notes:</h5><ul> 043 * <li class='note'>This class is thread safe and reusable. 044 * </ul> 045*/ 046public abstract class ConfigStore extends Context implements Closeable { 047 048 //------------------------------------------------------------------------------------------------------------------- 049 // Builder 050 //------------------------------------------------------------------------------------------------------------------- 051 052 /** 053 * Builder class. 054 */ 055 public abstract static class Builder extends Context.Builder { 056 057 /** 058 * Constructor, default settings. 059 */ 060 protected Builder() { 061 } 062 063 /** 064 * Copy constructor. 065 * 066 * @param copyFrom The bean to copy from. 067 */ 068 protected Builder(ConfigStore copyFrom) { 069 super(copyFrom); 070 } 071 072 /** 073 * Copy constructor. 074 * 075 * @param copyFrom The builder to copy from. 076 */ 077 protected Builder(Builder copyFrom) { 078 super(copyFrom); 079 } 080 081 @Override /* Context.Builder */ 082 public abstract Builder copy(); 083 084 //----------------------------------------------------------------------------------------------------------------- 085 // Properties 086 //----------------------------------------------------------------------------------------------------------------- 087 @Override /* Overridden from Builder */ 088 public Builder annotations(Annotation...values) { 089 super.annotations(values); 090 return this; 091 } 092 093 @Override /* Overridden from Builder */ 094 public Builder apply(AnnotationWorkList work) { 095 super.apply(work); 096 return this; 097 } 098 099 @Override /* Overridden from Builder */ 100 public Builder applyAnnotations(Object...from) { 101 super.applyAnnotations(from); 102 return this; 103 } 104 105 @Override /* Overridden from Builder */ 106 public Builder applyAnnotations(Class<?>...from) { 107 super.applyAnnotations(from); 108 return this; 109 } 110 111 @Override /* Overridden from Builder */ 112 public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) { 113 super.cache(value); 114 return this; 115 } 116 117 @Override /* Overridden from Builder */ 118 public Builder debug() { 119 super.debug(); 120 return this; 121 } 122 123 @Override /* Overridden from Builder */ 124 public Builder debug(boolean value) { 125 super.debug(value); 126 return this; 127 } 128 129 @Override /* Overridden from Builder */ 130 public Builder impl(Context value) { 131 super.impl(value); 132 return this; 133 } 134 135 @Override /* Overridden from Builder */ 136 public Builder type(Class<? extends org.apache.juneau.Context> value) { 137 super.type(value); 138 return this; 139 } 140 } 141 142 //------------------------------------------------------------------------------------------------------------------- 143 // Instance 144 //------------------------------------------------------------------------------------------------------------------- 145 146 private final ConcurrentHashMap<String,Set<ConfigStoreListener>> listeners = new ConcurrentHashMap<>(); 147 private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>(); 148 149 /** 150 * Constructor. 151 * 152 * @param builder The builder for this object. 153 */ 154 protected ConfigStore(Builder builder) { 155 super(builder); 156 } 157 158 /** 159 * Returns the contents of the configuration file. 160 * 161 * @param name The config file name. 162 * @return 163 * The contents of the configuration file. 164 * <br>A blank string if the config does not exist. 165 * <br>Never <jk>null</jk>. 166 * @throws IOException Thrown by underlying stream. 167 */ 168 public abstract String read(String name) throws IOException; 169 170 /** 171 * Saves the contents of the configuration file if the underlying storage hasn't been modified. 172 * 173 * @param name The config file name. 174 * @param expectedContents The expected contents of the file. 175 * @param newContents The new contents. 176 * @return 177 * If <jk>null</jk>, then we successfully stored the contents of the file. 178 * <br>Otherwise the contents of the file have changed and we return the new contents of the file. 179 * @throws IOException Thrown by underlying stream. 180 */ 181 public abstract String write(String name, String expectedContents, String newContents) throws IOException; 182 183 /** 184 * Checks whether the configuration with the specified name exists in this store. 185 * 186 * @param name The config name. 187 * @return <jk>true</jk> if the configuration with the specified name exists in this store. 188 */ 189 public abstract boolean exists(String name); 190 191 /** 192 * Registers a new listener on this store. 193 * 194 * @param name The configuration name to listen for. 195 * @param l The new listener. 196 * @return This object. 197 */ 198 public synchronized ConfigStore register(String name, ConfigStoreListener l) { 199 name = resolveName(name); 200 var s = listeners.computeIfAbsent( 201 name, 202 k -> synced(newSetFromMap(new IdentityHashMap<>())) 203 ); 204 s.add(l); 205 return this; 206 } 207 208 /** 209 * Unregisters a listener from this store. 210 * 211 * @param name The configuration name to listen for. 212 * @param l The listener to unregister. 213 * @return This object. 214 */ 215 public synchronized ConfigStore unregister(String name, ConfigStoreListener l) { 216 name = resolveName(name); 217 var s = listeners.get(name); 218 if (s != null) 219 s.remove(l); 220 return this; 221 } 222 223 /** 224 * Returns a map file containing the parsed contents of a configuration. 225 * 226 * @param name The configuration name. 227 * @return 228 * The parsed configuration. 229 * <br>Never <jk>null</jk>. 230 * @throws IOException Thrown by underlying stream. 231 */ 232 public synchronized ConfigMap getMap(String name) throws IOException { 233 name = resolveName(name); 234 var cm = configMaps.get(name); 235 if (cm != null) 236 return cm; 237 cm = new ConfigMap(this, name); 238 var cm2 = configMaps.putIfAbsent(name, cm); 239 if (cm2 != null) 240 return cm2; 241 register(name, cm); 242 return cm; 243 } 244 245 /** 246 * Called when the physical contents of a config file have changed. 247 * 248 * <p> 249 * Triggers calls to {@link ConfigStoreListener#onChange(String)} on all registered listeners. 250 * 251 * @param name The config name (e.g. the filename without the extension). 252 * @param contents The new contents. 253 * @return This object. 254 */ 255 public synchronized ConfigStore update(String name, String contents) { 256 name = resolveName(name); 257 var s = listeners.get(name); 258 if (s != null) 259 listeners.get(name).forEach(x -> x.onChange(contents)); 260 return this; 261 } 262 263 /** 264 * Convenience method for updating the contents of a file with lines. 265 * 266 * @param name The config name (e.g. the filename without the extension). 267 * @param contentLines The new contents. 268 * @return This object. 269 */ 270 public synchronized ConfigStore update(String name, String...contentLines) { 271 name = resolveName(name); 272 var sb = new StringBuilder(); 273 for (var l : contentLines) 274 sb.append(l).append('\n'); 275 return update(name, sb.toString()); 276 } 277 278 /** 279 * Subclasses can override this method to convert config names to internal forms. 280 * 281 * <p> 282 * For example, the {@link FileStore} class can take in both <js>"MyConfig"</js> and <js>"MyConfig.cfg"</js> 283 * as names that both resolve to <js>"MyConfig.cfg"</js>. 284 * 285 * @param name The name to resolve. 286 * @return The resolved name. 287 */ 288 protected String resolveName(String name) { 289 return name; 290 } 291}