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