http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java new file mode 100644 index 0000000..593a7e9 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java @@ -0,0 +1,60 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import org.apache.juneau.*; + +/** + * TODO + */ +public class ConfigFileContext extends Context { + + /** + * TODO + * + * @param propertyStore + */ + public ConfigFileContext(PropertyStore propertyStore) { + super(propertyStore); + } + + /** + * TODO + */ + public static final String CONFIGFILE_serializer = "ConfigFile.serializer"; + + /** + * TODO + */ + public static final String CONFIGFILE_parser = "ConfigFile.parser"; + + /** + * TODO + */ + public static final String CONFIGFILE_encoder = "ConfigFile.encoder"; + + /** + * TODO + */ + public static final String CONFIGFILE_readonly = "ConfigFile.readonly"; + + /** + * TODO + */ + public static final String CONFIGFILE_createIfNotExists = "ConfigFile.createIfNotExists"; + + /** + * TODO + */ + public static final String CONFIGFILE_wsDepth = "ConfigFile.wsDepth"; +}
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java new file mode 100644 index 0000000..3000a66 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java @@ -0,0 +1,30 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import java.io.*; + +/** + * Valid formats that can be passed to the {@link ConfigFile#serializeTo(Writer, ConfigFileFormat)} method. + */ +public enum ConfigFileFormat { + + /** Normal INI file format*/ + INI, + + /** Batch file with "set X" commands */ + BATCH, + + /** Shell script file with "export X" commands */ + SHELL; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java new file mode 100644 index 0000000..5986ebc --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java @@ -0,0 +1,821 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import static org.apache.juneau.ini.ConfigUtils.*; +import static org.apache.juneau.internal.ThrowableUtils.*; +import static org.apache.juneau.internal.StringUtils.*; + +import java.io.*; +import java.lang.reflect.*; +import java.nio.charset.*; +import java.util.*; +import java.util.concurrent.locks.*; + +import org.apache.juneau.*; +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; +import org.apache.juneau.svl.vars.*; + +/** + * Implementation class for {@link ConfigFile}. + */ +public final class ConfigFileImpl extends ConfigFile { + + private final File file; + private final Encoder encoder; + private final WriterSerializer serializer; + private final ReaderParser parser; + private final BeanSession pBeanSession; + private final Charset charset; + final List<ConfigFileListener> listeners = Collections.synchronizedList(new ArrayList<ConfigFileListener>()); + + private Map<String,Section> sections; // The actual data. + + private static final String DEFAULT = "default"; + + private final boolean readOnly; + + volatile boolean hasBeenModified = false; + private ReadWriteLock lock = new ReentrantReadWriteLock(); + + long modifiedTimestamp; + + /** + * Constructor. + * + * <p> + * Loads the contents of the specified file into this config file. + * + * <p> + * If file does not initially exist, this object will start off empty. + * + * @param file + * The INI file on disk. + * If <jk>null</jk>, create an in-memory config file. + * @param readOnly + * Make this configuration file read-only. + * Attempting to set any values on this config file will cause {@link UnsupportedOperationException} to be thrown. + * @param encoder + * The encoder to use for encoding sensitive values in this configuration file. + * If <jk>null</jk>, defaults to {@link XorEncoder#INSTANCE}. + * @param serializer + * The serializer to use for serializing POJOs in the {@link #put(String, Object)} method. + * If <jk>null</jk>, defaults to {@link JsonSerializer#DEFAULT}. + * @param parser + * The parser to use for parsing POJOs in the {@link #getObject(String,Class)} method. + * If <jk>null</jk>, defaults to {@link JsonParser#DEFAULT}. + * @param charset + * The charset on the files. + * If <jk>null</jk>, defaults to {@link Charset#defaultCharset()}. + * @throws IOException + */ + public ConfigFileImpl(File file, boolean readOnly, Encoder encoder, WriterSerializer serializer, ReaderParser parser, + Charset charset) throws IOException { + this.file = file; + this.encoder = encoder == null ? XorEncoder.INSTANCE : encoder; + this.serializer = serializer == null ? JsonSerializer.DEFAULT : serializer; + this.parser = parser == null ? JsonParser.DEFAULT : parser; + this.charset = charset == null ? Charset.defaultCharset() : charset; + load(); + this.readOnly = readOnly; + if (readOnly) { + this.sections = Collections.unmodifiableMap(this.sections); + for (Section s : sections.values()) + s.setReadOnly(); + } + this.pBeanSession = this.parser.getBeanContext().createSession(); + } + + /** + * Constructor. + * + * <p> + * Shortcut for calling <code><jk>new</jk> ConfigFileImpl(file, <jk>false</jk>, <jk>null</jk>, <jk>null</jk>, + * <jk>null</jk>, <jk>null</jk>);</code> + * + * @param file The config file. Does not need to exist. + * @throws IOException + */ + public ConfigFileImpl(File file) throws IOException { + this(file, false, null, null, null, null); + } + + /** + * Constructor. + * + * <p> + * Shortcut for calling <code><jk>new</jk> ConfigFileImpl(<jk>null</jk>, <jk>false</jk>, <jk>null</jk>, + * <jk>null</jk>, <jk>null</jk>, <jk>null</jk>);</code> + * + * @throws IOException + */ + public ConfigFileImpl() throws IOException { + this(null); + } + + @Override /* ConfigFile */ + public ConfigFileImpl loadIfModified() throws IOException { + if (file == null) + return this; + writeLock(); + try { + if (file.lastModified() > modifiedTimestamp) + load(); + } finally { + writeUnlock(); + } + return this; + } + + @Override /* ConfigFile */ + public ConfigFileImpl load() throws IOException { + Reader r = null; + if (file != null && file.exists()) + r = new InputStreamReader(new FileInputStream(file), charset); + else + r = new StringReader(""); + try { + load(r); + } finally { + r.close(); + } + return this; + } + + @Override /* ConfigFile */ + public ConfigFileImpl load(Reader r) throws IOException { + assertFieldNotNull(r, "r"); + writeLock(); + try { + this.sections = Collections.synchronizedMap(new LinkedHashMap<String,Section>()); + BufferedReader in = new BufferedReader(r); + try { + writeLock(); + hasBeenModified = false; + try { + sections.clear(); + String line = null; + Section section = getSection(null, true); + ArrayList<String> lines = new ArrayList<String>(); + boolean canAppend = false; + while ((line = in.readLine()) != null) { + if (isSection(line)) { + section.addLines(null, lines.toArray(new String[lines.size()])); + lines.clear(); + canAppend = false; + String sn = replaceUnicodeSequences(line.substring(line.indexOf('[')+1, line.indexOf(']')).trim()); + section = getSection(sn, true).addHeaderComments(section.removeTrailingComments()); + } else { + char c = line.isEmpty() ? 0 : line.charAt(0); + if ((c == ' ' || c == '\t') && canAppend && ! (isComment(line) || isAssignment(line))) + lines.add(lines.remove(lines.size()-1) + '\n' + line.substring(1)); + else { + lines.add(line); + if (isAssignment(line)) + canAppend = true; + else + canAppend = canAppend && ! (StringUtils.isEmpty(line) || isComment(line)); + } + } + } + section.addLines(null, lines.toArray(new String[lines.size()])); + in.close(); + if (hasBeenModified) // Set when values need to be encoded. + save(); + if (file != null) + modifiedTimestamp = file.lastModified(); + } finally { + writeUnlock(); + } + } finally { + in.close(); + } + } finally { + writeUnlock(); + } + for (ConfigFileListener l : listeners) + l.onLoad(this); + return this; + } + + @SuppressWarnings("hiding") + @Override /* ConfigFile */ + protected String serialize(Object value, Serializer serializer, boolean newline) throws SerializeException { + if (value == null) + return ""; + if (serializer == null) + serializer = this.serializer; + Class<?> c = value.getClass(); + if (isSimpleType(c)) + return value.toString(); + + String r = null; + if (newline) + r = "\n" + (String)serializer.serialize(value); + else + r = (String)serializer.serialize(value); + + if (r.startsWith("'")) + return r.substring(1, r.length()-1); + return r; + } + + @Override /* ConfigFile */ + @SuppressWarnings({ "unchecked", "hiding" }) + protected <T> T parse(String s, Parser parser, Type type, Type...args) throws ParseException { + + if (StringUtils.isEmpty(s)) + return null; + + if (isSimpleType(type)) + return (T)pBeanSession.convertToType(s, (Class<?>)type); + + char s1 = firstNonWhitespaceChar(s); + if (s1 != '[' && s1 != '{' && ! "null".equals(s)) + s = '\'' + s + '\''; + + if (parser == null) + parser = this.parser; + + return parser.parse(s, type, args); + } + + private static boolean isSimpleType(Type t) { + if (! (t instanceof Class)) + return false; + Class<?> c = (Class<?>)t; + return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum()); + } + + + //-------------------------------------------------------------------------------- + // Map methods + //-------------------------------------------------------------------------------- + + @Override /* Map */ + public Section get(Object key) { + if (StringUtils.isEmpty(key)) + key = DEFAULT; + readLock(); + try { + return sections.get(key); + } finally { + readUnlock(); + } + } + + @Override /* Map */ + public Section put(String key, Section section) { + Set<String> changes = createChanges(); + Section old = put(key, section, changes); + signalChanges(changes); + return old; + } + + private Section put(String key, Section section, Set<String> changes) { + if (StringUtils.isEmpty(key)) + key = DEFAULT; + writeLock(); + try { + Section prev = sections.put(key, section); + findChanges(changes, prev, section); + return prev; + } finally { + writeUnlock(); + } + } + + @Override /* Map */ + public void putAll(Map<? extends String,? extends Section> map) { + Set<String> changes = createChanges(); + writeLock(); + try { + for (Map.Entry<? extends String,? extends Section> e : map.entrySet()) + put(e.getKey(), e.getValue(), changes); + } finally { + writeUnlock(); + } + signalChanges(changes); + } + + @Override /* Map */ + public void clear() { + Set<String> changes = createChanges(); + writeLock(); + try { + for (Section s : values()) + findChanges(changes, s, null); + sections.clear(); + } finally { + writeUnlock(); + } + signalChanges(changes); + } + + @Override /* Map */ + public boolean containsKey(Object key) { + if (StringUtils.isEmpty(key)) + key = DEFAULT; + return sections.containsKey(key); + } + + @Override /* Map */ + public boolean containsValue(Object value) { + return sections.containsValue(value); + } + + @Override /* Map */ + public Set<Map.Entry<String,Section>> entrySet() { + + // We need to create our own set so that entries are removed correctly. + return new AbstractSet<Map.Entry<String,Section>>() { + @Override /* Map */ + public Iterator<Map.Entry<String,Section>> iterator() { + return new Iterator<Map.Entry<String,Section>>() { + Iterator<Map.Entry<String,Section>> i = sections.entrySet().iterator(); + Map.Entry<String,Section> i2; + + @Override /* Iterator */ + public boolean hasNext() { + return i.hasNext(); + } + + @Override /* Iterator */ + public Map.Entry<String,Section> next() { + i2 = i.next(); + return i2; + } + + @Override /* Iterator */ + public void remove() { + Set<String> changes = createChanges(); + findChanges(changes, i2.getValue(), null); + i.remove(); + signalChanges(changes); + } + }; + } + + @Override /* Map */ + public int size() { + return sections.size(); + } + }; + } + + @Override /* Map */ + public boolean isEmpty() { + return sections.isEmpty(); + } + + @Override /* Map */ + public Set<String> keySet() { + + // We need to create our own set so that sections are removed correctly. + return new AbstractSet<String>() { + @Override /* Set */ + public Iterator<String> iterator() { + return new Iterator<String>() { + Iterator<String> i = sections.keySet().iterator(); + String i2; + + @Override /* Iterator */ + public boolean hasNext() { + return i.hasNext(); + } + + @Override /* Iterator */ + public String next() { + i2 = i.next(); + return i2; + } + + @Override /* Iterator */ + public void remove() { + Set<String> changes = createChanges(); + findChanges(changes, sections.get(i2), null); + i.remove(); + signalChanges(changes); + } + }; + } + + @Override /* Set */ + public int size() { + return sections.size(); + } + }; + } + + @Override /* Map */ + public int size() { + return sections.size(); + } + + @Override /* Map */ + public Collection<Section> values() { + return new AbstractCollection<Section>() { + @Override /* Collection */ + public Iterator<Section> iterator() { + return new Iterator<Section>() { + Iterator<Section> i = sections.values().iterator(); + Section i2; + + @Override /* Iterator */ + public boolean hasNext() { + return i.hasNext(); + } + + @Override /* Iterator */ + public Section next() { + i2 = i.next(); + return i2; + } + + @Override /* Iterator */ + public void remove() { + Set<String> changes = createChanges(); + findChanges(changes, i2, null); + i.remove(); + signalChanges(changes); + } + }; + } + @Override /* Collection */ + public int size() { + return sections.size(); + } + }; + } + + @Override /* Map */ + public Section remove(Object key) { + Set<String> changes = createChanges(); + Section prev = remove(key, changes); + signalChanges(changes); + return prev; + } + + private Section remove(Object key, Set<String> changes) { + writeLock(); + try { + Section prev = sections.remove(key); + findChanges(changes, prev, null); + return prev; + } finally { + writeUnlock(); + } + } + + + //-------------------------------------------------------------------------------- + // API methods + //-------------------------------------------------------------------------------- + + @Override /* ConfigFile */ + public String get(String sectionName, String sectionKey) { + assertFieldNotNull(sectionKey, "sectionKey"); + Section s = get(sectionName); + if (s == null) + return null; + Object s2 = s.get(sectionKey); + return (s2 == null ? null : s2.toString()); + } + + @SuppressWarnings("hiding") + @Override /* ConfigFile */ + public String put(String sectionName, String sectionKey, Object value, Serializer serializer, boolean encoded, + boolean newline) throws SerializeException { + assertFieldNotNull(sectionKey, "sectionKey"); + Section s = getSection(sectionName, true); + return s.put(sectionKey, serialize(value, serializer, newline), encoded); + } + + @Override /* ConfigFile */ + public String put(String sectionName, String sectionKey, String value, boolean encoded) { + assertFieldNotNull(sectionKey, "sectionKey"); + Section s = getSection(sectionName, true); + return s.put(sectionKey, value, encoded); + } + + @Override /* ConfigFile */ + public String remove(String sectionName, String sectionKey) { + assertFieldNotNull(sectionKey, "sectionKey"); + Section s = getSection(sectionName, false); + if (s == null) + return null; + return s.remove(sectionKey); + } + + @Override /* ConfigFile */ + public ConfigFileImpl addLines(String section, String...lines) { + Set<String> changes = createChanges(); + writeLock(); + try { + getSection(section, true).addLines(changes, lines); + } finally { + writeUnlock(); + } + signalChanges(changes); + return this; + } + + @Override /* ConfigFile */ + public ConfigFileImpl addHeaderComments(String section, String...headerComments) { + writeLock(); + try { + if (headerComments == null) + headerComments = new String[0]; + getSection(section, true).addHeaderComments(Arrays.asList(headerComments)); + } finally { + writeUnlock(); + } + return this; + } + + @Override /* ConfigFile */ + public ConfigFileImpl clearHeaderComments(String section) { + writeLock(); + try { + Section s = getSection(section, false); + if (s != null) + s.clearHeaderComments(); + } finally { + writeUnlock(); + } + return this; + } + + @Override /* ConfigFile */ + public Section getSection(String name) { + return getSection(name, false); + } + + @Override /* ConfigFile */ + public Section getSection(String name, boolean create) { + if (StringUtils.isEmpty(name)) + name = DEFAULT; + Section s = sections.get(name); + if (s != null) + return s; + if (create) { + s = new Section().setParent(this).setName(name); + sections.put(name, s); + return s; + } + return null; + } + + @Override /* ConfigFile */ + public ConfigFileImpl addSection(String name) { + writeLock(); + try { + getSection(name, true); + } finally { + writeUnlock(); + } + return this; + } + + @Override /* ConfigFile */ + public ConfigFile setSection(String name, Map<String,String> contents) { + writeLock(); + try { + put(name, new Section(contents).setParent(this).setName(name)); + } finally { + writeUnlock(); + } + return this; + } + + @Override /* ConfigFile */ + public ConfigFileImpl removeSection(String name) { + Set<String> changes = createChanges(); + writeLock(); + try { + Section prev = sections.remove(name); + if (changes != null && prev != null) + findChanges(changes, prev, null); + } finally { + writeUnlock(); + } + signalChanges(changes); + return this; + } + + @Override /* ConfigFile */ + public Set<String> getSectionKeys(String sectionName) { + Section s = get(sectionName); + if (s == null) + return null; + return s.keySet(); + } + + @Override /* ConfigFile */ + public boolean isEncoded(String key) { + assertFieldNotNull(key, "key"); + String section = getSectionName(key); + Section s = getSection(section, false); + if (s == null) + return false; + return s.isEncoded(getSectionKey(key)); + } + + @Override /* ConfigFile */ + public ConfigFileImpl save() throws IOException { + writeLock(); + try { + if (file == null) + throw new UnsupportedOperationException("No backing file specified for config file."); + Writer out = new OutputStreamWriter(new FileOutputStream(file), charset); + try { + serializeTo(out); + hasBeenModified = false; + modifiedTimestamp = file.lastModified(); + } finally { + out.close(); + } + for (ConfigFileListener l : listeners) + l.onSave(this); + return this; + } finally { + writeUnlock(); + } + } + + @Override /* ConfigFile */ + public ConfigFileImpl serializeTo(Writer out, ConfigFileFormat format) throws IOException { + readLock(); + try { + PrintWriter pw = (out instanceof PrintWriter ? (PrintWriter)out : new PrintWriter(out)); + for (Section s : sections.values()) + s.writeTo(pw, format); + pw.flush(); + pw.close(); + out.close(); + } finally { + readUnlock(); + } + return this; + } + + void setHasBeenModified() { + hasBeenModified = true; + } + + @Override /* ConfigFile */ + public String toString() { + try { + StringWriter sw = new StringWriter(); + toWritable().writeTo(sw); + return sw.toString(); + } catch (IOException e) { + return e.getLocalizedMessage(); + } + } + + @Override /* ConfigFile */ + public ConfigFile addListener(ConfigFileListener listener) { + assertFieldNotNull(listener, "listener"); + writeLock(); + try { + this.listeners.add(listener); + return this; + } finally { + writeUnlock(); + } + } + + List<ConfigFileListener> getListeners() { + return listeners; + } + + @Override /* ConfigFile */ + public Writable toWritable() { + return new ConfigFileWritable(this); + } + + @Override /* ConfigFile */ + public ConfigFile merge(ConfigFile cf) { + assertFieldNotNull(cf, "cf"); + Set<String> changes = createChanges(); + writeLock(); + try { + for (String sectionName : this.keySet()) + if (! cf.containsKey(sectionName)) + remove(sectionName, changes); + + for (Map.Entry<String,Section> e : cf.entrySet()) + put(e.getKey(), e.getValue(), changes); + + } finally { + writeUnlock(); + } + signalChanges(changes); + return this; + } + + Encoder getEncoder() { + return encoder; + } + + @Override /* ConfigFile */ + protected BeanSession getBeanSession() { + return pBeanSession; + } + + @Override /* ConfigFile */ + protected void readLock() { + lock.readLock().lock(); + } + + @Override /* ConfigFile */ + protected void readUnlock() { + lock.readLock().unlock(); + } + + private void writeLock() { + if (readOnly) + throw new UnsupportedOperationException("Cannot modify read-only ConfigFile."); + lock.writeLock().lock(); + hasBeenModified = true; + } + + private void writeUnlock() { + lock.writeLock().unlock(); + } + + @Override /* ConfigFile */ + public ConfigFile getResolving(VarResolver vr) { + assertFieldNotNull(vr, "vr"); + return new ConfigFileWrapped(this, vr); + } + + @Override /* ConfigFile */ + public ConfigFile getResolving(VarResolverSession vs) { + assertFieldNotNull(vs, "vs"); + return new ConfigFileWrapped(this, vs); + } + + @Override /* ConfigFile */ + public ConfigFile getResolving() { + return getResolving( + new VarResolverBuilder() + .vars(SystemPropertiesVar.class, EnvVariablesVar.class, SwitchVar.class, IfVar.class, ConfigFileVar.class, + IfVar.class, SwitchVar.class) + .contextObject(ConfigFileVar.SESSION_config, this) + .build() + ); + } + + /* + * Finds the keys that are different between the two sections and adds it to + * the specified set. + */ + private static void findChanges(Set<String> s, Section a, Section b) { + if (s == null) + return; + String sname = (a == null ? b.name : a.name); + if (a == null) { + for (String k : b.keySet()) + s.add(getFullKey(sname, k)); + } else if (b == null) { + for (String k : a.keySet()) + s.add(getFullKey(sname, k)); + } else { + for (String k : a.keySet()) + addChange(s, sname, k, a.get(k), b.get(k)); + for (String k : b.keySet()) + addChange(s, sname, k, a.get(k), b.get(k)); + } + } + + private static void addChange(Set<String> changes, String section, String key, String oldVal, String newVal) { + if (! isEquals(oldVal, newVal)) + changes.add(getFullKey(section, key)); + } + + private Set<String> createChanges() { + return (listeners.size() > 0 ? new LinkedHashSet<String>() : null); + } + + private void signalChanges(Set<String> changes) { + if (changes != null && ! changes.isEmpty()) + for (ConfigFileListener l : listeners) + l.onChange(this, changes); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java new file mode 100644 index 0000000..cfed21a --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java @@ -0,0 +1,46 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import java.util.*; + +/** + * Listener that can be used to listen for change events in config files. + * + * <p> + * Use the {@link ConfigFile#addListener(ConfigFileListener)} method to register listeners. + */ +public class ConfigFileListener { + + /** + * Gets called immediately after a config file has been loaded. + * + * @param cf The config file being loaded. + */ + public void onLoad(ConfigFile cf) {} + + /** + * Gets called immediately after a config file has been saved. + * + * @param cf The config file being saved. + */ + public void onSave(ConfigFile cf) {} + + /** + * Signifies that the specified values have changed. + * + * @param cf The config file being modified. + * @param changes The full keys (e.g. <js>"Section/key"</js>) of entries that have changed in the config file. + */ + public void onChange(ConfigFile cf, Set<String> changes) {} +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java new file mode 100644 index 0000000..52fe0ea --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java @@ -0,0 +1,69 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import org.apache.juneau.svl.*; + +/** + * Config file variable resolver. + * + * <p> + * The format for this var is <js>"$C{key[,defaultValue]}"</js>. + * See {@link ConfigFile#getString(String)} for the format of the key. + * + * <p> + * This variable resolver requires that a {@link ConfigFile} object be set as a context object on the resolver or a + * session object on the resolver session. + * + * <h5 class='section'>Example:</h5> + * <p class='bcode'> + * <jc>// Create a config file object.</jc> + * ConfigFile configFile = new ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); + * + * <jc>// Create a variable resolver that resolves config file entries (e.g. "$C{MySection/myKey}")</jc> + * VarResolver r = <jk>new</jk> VarResolver().addVars(ConfigVar.<js>class</js>) + * .addContextObject(<jsf>SESSION_config</jsf>, configFile); + * + * <jc>// Use it!</jc> + * System.<jsf>out</jsf>.println(r.resolve(<js>"Value for myKey in section MySection is $C{MySection/myKey}"</js>)); + * </p> + * + * <p> + * Since this is a {@link SimpleVar}, any variables contained in the result will be recursively resolved. + * Likewise, if the arguments contain any variables, those will be resolved before they are passed to this var. + * + * @see org.apache.juneau.ini.ConfigFile + * @see org.apache.juneau.svl + */ +public class ConfigFileVar extends DefaultingVar { + + /** + * The name of the session or context object that identifies the {@link ConfigFile} object. + */ + public static final String SESSION_config = "config"; + + /** The name of this variable. */ + public static final String NAME = "C"; + + /** + * Constructor. + */ + public ConfigFileVar() { + super(NAME); + } + + @Override /* Var */ + public String resolve(VarResolverSession session, String key) { + return session.getSessionObject(ConfigFile.class, SESSION_config).getString(key); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java new file mode 100644 index 0000000..da8e83b --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java @@ -0,0 +1,290 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import static org.apache.juneau.internal.ThrowableUtils.*; + +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; + +/** + * Wraps an instance of {@link ConfigFileImpl} in an interface that will automatically replace {@link VarResolver} + * variables. + * + * <p> + * The {@link ConfigFile#getResolving(VarResolver)} returns an instance of this class. + * + * <p> + * This class overrides the {@link #getString(String, String)} to resolve string variables. + * All other method calls are passed through to the inner config file. + */ +public final class ConfigFileWrapped extends ConfigFile { + + private final ConfigFileImpl cf; + private final VarResolverSession vs; + + ConfigFileWrapped(ConfigFileImpl cf, VarResolver vr) { + this.cf = cf; + this.vs = vr.builder() + .vars(ConfigFileVar.class) + .contextObject(ConfigFileVar.SESSION_config, cf) + .build() + .createSession(); + } + + ConfigFileWrapped(ConfigFileImpl cf, VarResolverSession vs) { + this.cf = cf; + this.vs = vs; + } + + @Override /* ConfigFile */ + public void clear() { + cf.clear(); + } + + @Override /* ConfigFile */ + public boolean containsKey(Object key) { + return cf.containsKey(key); + } + + @Override /* ConfigFile */ + public boolean containsValue(Object value) { + return cf.containsValue(value); + } + + @Override /* ConfigFile */ + public Set<java.util.Map.Entry<String,Section>> entrySet() { + return cf.entrySet(); + } + + @Override /* ConfigFile */ + public Section get(Object key) { + return cf.get(key); + } + + @Override /* ConfigFile */ + public boolean isEmpty() { + return cf.isEmpty(); + } + + @Override /* ConfigFile */ + public Set<String> keySet() { + return cf.keySet(); + } + + @Override /* ConfigFile */ + public Section put(String key, Section value) { + return cf.put(key, value); + } + + @Override /* ConfigFile */ + public void putAll(Map<? extends String,? extends Section> map) { + cf.putAll(map); + } + + @Override /* ConfigFile */ + public Section remove(Object key) { + return cf.remove(key); + } + + @Override /* ConfigFile */ + public int size() { + return cf.size(); + } + + @Override /* ConfigFile */ + public Collection<Section> values() { + return cf.values(); + } + + @Override /* ConfigFile */ + public ConfigFile loadIfModified() throws IOException { + cf.loadIfModified(); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile load() throws IOException { + cf.load(); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile load(Reader r) throws IOException { + cf.load(r); + return this; + } + + + @Override /* ConfigFile */ + public boolean isEncoded(String key) { + return cf.isEncoded(key); + } + + @Override /* ConfigFile */ + public ConfigFile addLines(String section, String... lines) { + cf.addLines(section, lines); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile addHeaderComments(String section, String... headerComments) { + cf.addHeaderComments(section, headerComments); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile clearHeaderComments(String section) { + cf.clearHeaderComments(section); + return this; + } + + @Override /* ConfigFile */ + public Section getSection(String name) { + return cf.getSection(name); + } + + @Override /* ConfigFile */ + public Section getSection(String name, boolean create) { + return cf.getSection(name, create); + } + + @Override /* ConfigFile */ + public ConfigFile addSection(String name) { + cf.addSection(name); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile setSection(String name, Map<String,String> contents) { + cf.setSection(name, contents); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile removeSection(String name) { + cf.removeSection(name); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile save() throws IOException { + cf.save(); + return this; + } + + @Override /* ConfigFile */ + public ConfigFile serializeTo(Writer out, ConfigFileFormat format) throws IOException { + cf.serializeTo(out, format); + return this; + } + + @Override /* ConfigFile */ + public String toString() { + return cf.toString(); + } + + @Override /* ConfigFile */ + public ConfigFile getResolving(VarResolver varResolver) { + assertFieldNotNull(varResolver, "vr"); + return new ConfigFileWrapped(cf, varResolver); + } + + @Override /* ConfigFile */ + public ConfigFile getResolving(VarResolverSession varSession) { + assertFieldNotNull(varSession, "vs"); + return new ConfigFileWrapped(cf, varSession); + } + + @Override /* ConfigFile */ + public ConfigFile getResolving() { + return new ConfigFileWrapped(cf, VarResolver.DEFAULT); + } + + @Override /* ConfigFile */ + public ConfigFile addListener(ConfigFileListener listener) { + cf.addListener(listener); + return this; + } + + @Override /* ConfigFile */ + public Writable toWritable() { + return cf.toWritable(); + } + + @Override /* ConfigFile */ + public ConfigFile merge(ConfigFile newCf) { + cf.merge(newCf); + return this; + } + + @Override /* ConfigFile */ + protected BeanSession getBeanSession() { + return cf.getBeanSession(); + } + + @Override /* ConfigFile */ + public String get(String sectionName, String sectionKey) { + String s = cf.get(sectionName, sectionKey); + if (s == null) + return null; + return vs.resolve(s); + } + + @Override /* ConfigFile */ + public String put(String sectionName, String sectionKey, String value, boolean encoded) { + return cf.put(sectionName, sectionKey, value, encoded); + } + + @Override /* ConfigFile */ + public String put(String sectionName, String sectionKey, Object value, Serializer serializer, boolean encoded, + boolean newline) throws SerializeException { + return cf.put(sectionName, sectionKey, value, serializer, encoded, newline); + } + + @Override /* ConfigFile */ + public String remove(String sectionName, String sectionKey) { + return cf.remove(sectionName, sectionKey); + } + + @Override /* ConfigFile */ + public Set<String> getSectionKeys(String sectionName) { + return cf.getSectionKeys(sectionName); + } + + @Override /* ConfigFile */ + protected void readLock() { + cf.readLock(); + } + + @Override /* ConfigFile */ + protected void readUnlock() { + cf.readUnlock(); + } + + @Override /* ConfigFile */ + protected String serialize(Object o, Serializer s, boolean newline) throws SerializeException { + return cf.serialize(o, s, newline); + } + + @Override /* ConfigFile */ + protected <T> T parse(String s, Parser parser, Type type, Type... args) throws ParseException { + return cf.parse(s, parser, type, args); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java new file mode 100644 index 0000000..b8c504c --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java @@ -0,0 +1,45 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import java.io.*; + +import org.apache.juneau.*; +import org.apache.juneau.http.*; + +/** + * Wraps a {@link ConfigFile} in a {@link Writable} to be rendered as plain text. + */ +class ConfigFileWritable implements Writable { + + private ConfigFileImpl cf; + + protected ConfigFileWritable(ConfigFileImpl cf) { + this.cf = cf; + } + + @Override /* Writable */ + public void writeTo(Writer out) throws IOException { + cf.readLock(); + try { + cf.serializeTo(out); + } finally { + cf.readUnlock(); + } + } + + @Override /* Writable */ + public MediaType getMediaType() { + return MediaType.PLAIN; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java new file mode 100644 index 0000000..9e635f4 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java @@ -0,0 +1,92 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +/** + * Internal utility methods. + */ +public class ConfigUtils { + + static final String getSectionName(String key) { + int i = key.indexOf('/'); + if (i == -1) + return "default"; + return key.substring(0, i); + } + + static final String getSectionKey(String key) { + int i = key.indexOf('/'); + if (i == -1) + return key; + return key.substring(i+1); + } + + static final String getFullKey(String section, String key) { + if (section.equals("default")) + return key; + return section + '/' + key; + } + + static final boolean isComment(String line) { + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (! Character.isWhitespace(c)) + return c == '#'; + } + return false; + } + + static final boolean isAssignment(String line) { + int S1 = 1; // Looking for char; + int S2 = 2; // Found char, looking for whitespace or = + int S3 = 3; // Found whitespace, looking for = + int state = S1; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (state == S1) { + if (! Character.isWhitespace(c)) + state = S2; + } else if (state == S2) { + if (c == '=') + return true; + if (Character.isWhitespace(c)) + state = S3; + } else if (state == S3) { + if (c == '=') + return true; + } + } + return false; + } + + static final boolean isSection(String line) { + int S1 = 1; // Looking for [; + int S2 = 2; // Found [, looking for ] + int state = S1; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (state == S1) { + if (! Character.isWhitespace(c)) { + if (c == '[') + state = S2; + else + return false; + } + } else if (state == S2) { + if (c == ']') + return true; + } + } + return false; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java new file mode 100644 index 0000000..9b73b25 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java @@ -0,0 +1,37 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +/** + * API for defining a string encoding/decoding mechanism for entries in {@link ConfigFile}. + */ +public interface Encoder { + + /** + * Encode a string. + * + * @param fieldName The field name being encoded. + * @param in The unencoded input string. + * @return The encoded output string. + */ + public String encode(String fieldName, String in); + + /** + * Decode a string. + * + * @param fieldName The field name being decoded. + * @param in The encoded input string. + * @return The decoded output string. + */ + public String decode(String fieldName, String in); +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java new file mode 100644 index 0000000..87526c1 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java @@ -0,0 +1,48 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import java.util.*; + +/** + * Listener that can be used to listen for change events for a specific entry in a config file. + * + * <p> + * Use the {@link ConfigFile#addListener(ConfigFileListener)} method to register listeners. + */ +public class EntryListener extends ConfigFileListener { + + private String fullKey; + + /** + * Constructor. + * + * @param fullKey The key in the config file to listen for changes on. + */ + public EntryListener(String fullKey) { + this.fullKey = fullKey; + } + + @Override /* ConfigFileListener */ + public void onChange(ConfigFile cf, Set<String> changes) { + if (changes.contains(fullKey)) + onChange(cf); + } + + /** + * Signifies that the config file entry changed. + * + * @param cf The config file being changed. + */ + public void onChange(ConfigFile cf) {} +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java new file mode 100644 index 0000000..39108a3 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java @@ -0,0 +1,575 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import static org.apache.juneau.ini.ConfigFileFormat.*; +import static org.apache.juneau.ini.ConfigUtils.*; +import static org.apache.juneau.internal.StringUtils.*; + +import java.io.*; +import java.util.*; +import java.util.concurrent.locks.*; + +import org.apache.juneau.annotation.*; + +/** + * Defines a section in a config file. + */ +public class Section implements Map<String,String> { + + private ConfigFileImpl configFile; + String name; // The config section name, or "default" if the default section. Never null. + + // The data structures that make up this object. + // These must be kept synchronized. + private LinkedList<String> lines = new LinkedList<String>(); + private List<String> headerComments = new LinkedList<String>(); + private Map<String,String> entries; + + private ReadWriteLock lock = new ReentrantReadWriteLock(); + private boolean readOnly; + + /** + * Constructor. + */ + public Section() { + this.entries = new LinkedHashMap<String,String>(); + } + + /** + * Constructor with predefined contents. + * + * @param contents Predefined contents to copy into this section. + */ + public Section(Map<String,String> contents) { + this.entries = new LinkedHashMap<String,String>(contents); + } + + Section setReadOnly() { + // This method is only called once from ConfigFileImpl constructor. + this.readOnly = true; + this.entries = Collections.unmodifiableMap(entries); + return this; + } + + /** + * Sets the config file that this section belongs to. + * + * @param configFile The config file that this section belongs to. + * @return This object (for method chaining). + */ + @ParentProperty + public Section setParent(ConfigFileImpl configFile) { + this.configFile = configFile; + return this; + } + + /** + * Sets the section name + * + * @param name The section name. + * @return This object (for method chaining). + */ + @NameProperty + public Section setName(String name) { + this.name = name; + return this; + } + + + //-------------------------------------------------------------------------------- + // Map methods + //-------------------------------------------------------------------------------- + + @Override /* Map */ + public void clear() { + Set<String> changes = createChanges(); + writeLock(); + try { + if (changes != null) + for (String k : keySet()) + changes.add(getFullKey(name, k)); + entries.clear(); + lines.clear(); + headerComments.clear(); + } finally { + writeUnlock(); + } + signalChanges(changes); + } + + @Override /* Map */ + public boolean containsKey(Object key) { + return entries.containsKey(key); + } + + @Override /* Map */ + public boolean containsValue(Object value) { + return entries.containsValue(value); + } + + @Override /* Map */ + public Set<Map.Entry<String,String>> entrySet() { + + // We need to create our own set so that entries are removed correctly. + return new AbstractSet<Map.Entry<String,String>>() { + @Override /* Set */ + public Iterator<Map.Entry<String,String>> iterator() { + return new Iterator<Map.Entry<String,String>>() { + Iterator<Map.Entry<String,String>> i = entries.entrySet().iterator(); + Map.Entry<String,String> i2; + + @Override /* Iterator */ + public boolean hasNext() { + return i.hasNext(); + } + + @Override /* Iterator */ + public Map.Entry<String,String> next() { + i2 = i.next(); + return i2; + } + + @Override /* Iterator */ + public void remove() { + Set<String> changes = createChanges(); + String key = i2.getKey(), val = i2.getValue(); + addChange(changes, key, val, null); + writeLock(); + try { + i.remove(); + removeLine(key); + } finally { + writeUnlock(); + } + signalChanges(changes); + } + }; + } + + @Override /* Set */ + public int size() { + return entries.size(); + } + }; + } + + @Override /* Map */ + public String get(Object key) { + String s = entries.get(key); + return s; + } + + @Override /* Map */ + public boolean isEmpty() { + return entries.isEmpty(); + } + + @Override /* Map */ + public Set<String> keySet() { + + // We need to create our own set so that sections are removed correctly. + return new AbstractSet<String>() { + @Override /* Set */ + public Iterator<String> iterator() { + return new Iterator<String>() { + Iterator<String> i = entries.keySet().iterator(); + String i2; + + @Override /* Iterator */ + public boolean hasNext() { + return i.hasNext(); + } + + @Override /* Iterator */ + public String next() { + i2 = i.next(); + return i2; + } + + @Override /* Iterator */ + public void remove() { + Set<String> changes = createChanges(); + String key = i2; + String val = entries.get(key); + addChange(changes, key, val, null); + writeLock(); + try { + i.remove(); + removeLine(key); + } finally { + writeUnlock(); + } + signalChanges(changes); + } + }; + } + + @Override /* Set */ + public int size() { + return entries.size(); + } + }; + } + + @Override /* Map */ + public String put(String key, String value) { + return put(key, value, false); + } + + /** + * Sets the specified value in this section. + * + * @param key The section key. + * @param value The new value. + * @param encoded Whether this value should be encoded during save. + * @return The previous value. + */ + public String put(String key, String value, boolean encoded) { + Set<String> changes = createChanges(); + String s = put(key, value, encoded, changes); + signalChanges(changes); + return s; + } + + String put(String key, String value, boolean encoded, Set<String> changes) { + writeLock(); + try { + addLine(key, encoded); + String prev = entries.put(key, value); + addChange(changes, key, prev, value); + return prev; + } finally { + writeUnlock(); + } + } + + @Override /* Map */ + public void putAll(Map<? extends String,? extends String> map) { + Set<String> changes = createChanges(); + for (Map.Entry<? extends String,? extends String> e : map.entrySet()) + put(e.getKey(), e.getValue(), false, changes); + signalChanges(changes); + } + + @Override /* Map */ + public String remove(Object key) { + Set<String> changes = createChanges(); + String old = remove(key, changes); + signalChanges(changes); + return old; + } + + String remove(Object key, Set<String> changes) { + writeLock(); + try { + String prev = entries.remove(key); + addChange(changes, key.toString(), prev, null); + removeLine(key.toString()); + return prev; + } finally { + writeUnlock(); + } + } + + private void removeLine(String key) { + for (Iterator<String> i = lines.iterator(); i.hasNext();) { + String k = i.next(); + if (k.startsWith("*") || k.startsWith(">")) { + if (k.substring(1).equals(key)) { + i.remove(); + break; + } + } + } + } + + @Override /* Map */ + public int size() { + return entries.size(); + } + + @Override /* Map */ + public Collection<String> values() { + return Collections.unmodifiableCollection(entries.values()); + } + + + //-------------------------------------------------------------------------------- + // API methods + //-------------------------------------------------------------------------------- + + /** + * Returns <jk>true</jk> if the specified entry is encoded. + * + * @param key The key. + * @return <jk>true</jk> if the specified entry is encoded. + */ + public boolean isEncoded(String key) { + readLock(); + try { + for (String s : lines) + if (s.length() > 1) + if (s.substring(1).equals(key)) + return s.charAt(0) == '*'; + return false; + } finally { + readUnlock(); + } + } + + /** + * Adds header comments to this section. + * + * @see ConfigFile#addHeaderComments(String, String...) for a description. + * @param comments The comment lines to add to this section. + * @return This object (for method chaining). + */ + public Section addHeaderComments(List<String> comments) { + writeLock(); + try { + for (String c : comments) { + if (c == null) + c = ""; + if (! c.startsWith("#")) + c = "#" + c; + this.headerComments.add(c); + } + return this; + } finally { + writeUnlock(); + } + } + + /** + * Removes all header comments from this section. + */ + public void clearHeaderComments() { + writeLock(); + try { + this.headerComments.clear(); + } finally { + writeUnlock(); + } + } + + /** + * Serialize this section. + * + * @param out What to serialize to. + * @param format The format (e.g. INI, BATCH, SHELL). + */ + public void writeTo(PrintWriter out, ConfigFileFormat format) { + readLock(); + try { + if (format == INI) { + for (String s : headerComments) + out.append(s).println(); + if (! name.equals("default")) + out.append('[').append(name).append(']').println(); + for (String l : lines) { + char c = (l.length() > 0 ? l.charAt(0) : 0); + if (c == '>' || c == '*'){ + boolean encode = c == '*'; + String key = l.substring(1); + String val = entries.get(key); + if (val.indexOf('\n') != -1) + val = val.replaceAll("(\\r?\\n)", "$1\t"); + if (val.indexOf('=') != -1) + val = val.replace("=", "\\u003D"); + if (val.indexOf('#') != -1) + val = val.replace("#", "\\u0023"); + out.append(key); + if (encode) + out.append('*'); + out.append(" = "); + if (encode) + out.append('{').append(configFile.getEncoder().encode(key, val)).append('}'); + else + out.append(val); + out.println(); + } else { + out.append(l).println(); + } + } + + } else if (format == BATCH) { + String section = name.replaceAll("\\.\\/", "_"); + for (String l : headerComments) { + l = trimComment(l); + if (! l.isEmpty()) + out.append("rem ").append(l); + out.println(); + } + for (String l : lines) { + char c = (l.length() > 0 ? l.charAt(0) : 0); + if (c == '>' || c == '*') { + String key = l.substring(1); + String val = entries.get(key); + out.append("set "); + if (! name.equals("default")) + out.append(section).append('_'); + out.append(key.replaceAll("\\.\\/", "_")).append(" = ").append(val).println(); + } else { + l = trimComment(l); + if (! l.isEmpty()) + out.append("rem ").append(l); + out.println(); + } + } + + } else if (format == SHELL) { + String section = name.replaceAll("\\.\\/", "_"); + for (String l : headerComments) { + l = trimComment(l); + if (! l.isEmpty()) + out.append("# ").append(l); + out.println(); + } + for (String l : lines) { + char c = (l.length() > 0 ? l.charAt(0) : 0); + if (c == '>' || c == '*'){ + String key = l.substring(1); + String val = entries.get(key).replaceAll("\\\\", "\\\\\\\\"); + out.append("export "); + if (! name.equals("default")) + out.append(section).append('_'); + out.append(key.replaceAll("\\.\\/", "_")).append('=').append('"').append(val).append('"').println(); + } else { + l = trimComment(l); + if (! l.isEmpty()) + out.append("# ").append(l); + out.println(); + } + } + } + } finally { + readUnlock(); + } + } + + + //-------------------------------------------------------------------------------- + // Protected methods used by ConfigFile + //-------------------------------------------------------------------------------- + + /* + * Add lines to this section. + */ + Section addLines(Set<String> changes, String...l) { + writeLock(); + try { + if (l == null) + l = new String[0]; + for (int i = 0; i < l.length; i++) { + String line = l[i]; + if (line == null) + line = ""; + if (isComment(line)) + this.lines.add(line); + else if (isAssignment(line)) { + // Key/value pairs are stored as either ">key" or "*key"; + String key = replaceUnicodeSequences(line.substring(0, line.indexOf('=')).trim()); + String val = replaceUnicodeSequences(line.substring(line.indexOf('=')+1).trim()); + boolean encoded = key.length() > 1 && key.endsWith("*"); + if (encoded) { + key = key.substring(0, key.lastIndexOf('*')); + String v = val.toString().trim(); + if (v.startsWith("{") && v.endsWith("}")) + val = configFile.getEncoder().decode(key, v.substring(1, v.length()-1)); + else + configFile.setHasBeenModified(); + } + if (containsKey(key)) { + entries.remove(key); + lines.remove('*' + key); + lines.remove('>' + key); + } + lines.add((encoded ? '*' : '>') + key); + addChange(changes, key, entries.put(key, val), val); + } else { + this.lines.add(line); + } + } + return this; + } finally { + writeUnlock(); + } + } + + /* + * Remove all "#*" lines at the end of this section so they can + * be associated with the next section. + */ + List<String> removeTrailingComments() { + LinkedList<String> l = new LinkedList<String>(); + while ((! lines.isEmpty()) && lines.getLast().startsWith("#")) + l.addFirst(lines.removeLast()); + return l; + } + + + //-------------------------------------------------------------------------------- + // Private methods + //-------------------------------------------------------------------------------- + + private void addLine(String key, boolean encoded) { + for (Iterator<String> i = lines.iterator(); i.hasNext();) { + String k = i.next(); + if ((k.startsWith("*") || k.startsWith(">")) && k.substring(1).equals(key)) { + if (k.startsWith("*") && encoded || k.startsWith(">") && ! encoded) + return; + i.remove(); + } + } + lines.add((encoded ? "*" : ">") + key); + } + + private void readLock() { + lock.readLock().lock(); + } + + private void readUnlock() { + lock.readLock().unlock(); + } + + private void writeLock() { + if (readOnly) + throw new UnsupportedOperationException("Cannot modify read-only ConfigFile."); + lock.writeLock().lock(); + } + + private void writeUnlock() { + lock.writeLock().unlock(); + } + + private static String trimComment(String s) { + return s.replaceAll("^\\s*\\#\\s*", "").trim(); + } + + private Set<String> createChanges() { + return (configFile != null && configFile.getListeners().size() > 0 ? new LinkedHashSet<String>() : null); + } + + private void signalChanges(Set<String> changes) { + if (changes != null && ! changes.isEmpty()) + for (ConfigFileListener l : configFile.getListeners()) + l.onChange(configFile, changes); + } + + private void addChange(Set<String> changes, String key, String oldVal, String newVal) { + if (changes != null) + if (! isEquals(oldVal, newVal)) + changes.add(getFullKey(name, key)); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java new file mode 100644 index 0000000..aec28c1 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java @@ -0,0 +1,63 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import static org.apache.juneau.internal.StringUtils.*; + +import java.util.*; + +/** + * Listener that can be used to listen for change events for a specific section in a config file. + * + * <p> + * Use the {@link ConfigFile#addListener(ConfigFileListener)} method to register listeners. + */ +public class SectionListener extends ConfigFileListener { + + private boolean isDefault; + private String prefix; + + /** + * Constructor. + * + * @param section The name of the section in the config file to listen to. + */ + public SectionListener(String section) { + isDefault = isEmpty(section); + prefix = isDefault ? null : (section + '/'); + } + + @Override /* ConfigFileListener */ + public void onChange(ConfigFile cf, Set<String> changes) { + for (String c : changes) { + if (isDefault) { + if (c.indexOf('/') == -1) { + onChange(cf); + return; + } + } else { + if (c.startsWith(prefix)) { + onChange(cf); + return; + } + } + } + } + + /** + * Signifies that the config file entry changed. + * + * @param cf The config file being modified. + */ + public void onChange(ConfigFile cf) {} +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java new file mode 100644 index 0000000..bbd6b3d --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java @@ -0,0 +1,51 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you 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.apache.juneau.ini; + +import static org.apache.juneau.internal.StringUtils.*; +import static org.apache.juneau.internal.IOUtils.*; + +/** + * Simply XOR+Base64 encoder for obscuring passwords and other sensitive data in INI config files. + * + * <p> + * This is not intended to be used as strong encryption. + */ +public final class XorEncoder implements Encoder { + + /** Reusable XOR-Encoder instance. */ + public static final XorEncoder INSTANCE = new XorEncoder(); + + private static final String key = System.getProperty("org.apache.juneau.ini.XorEncoder.key", + "nuy7og796Vh6G9O6bG230SHK0cc8QYkH"); // The super-duper-secret key + + @Override /* Encoder */ + public String encode(String fieldName, String in) { + byte[] b = in.getBytes(UTF8); + for (int i = 0; i < b.length; i++) { + int j = i % key.length(); + b[i] = (byte)(b[i] ^ key.charAt(j)); + } + return base64Encode(b); + } + + @Override /* Encoder */ + public String decode(String fieldName, String in) { + byte[] b = base64Decode(in); + for (int i = 0; i < b.length; i++) { + int j = i % key.length(); + b[i] = (byte)(b[i] ^ key.charAt(j)); + } + return new String(b, UTF8); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png new file mode 100644 index 0000000..531f280 Binary files /dev/null and b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png differ http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png new file mode 100644 index 0000000..7f5a4b3 Binary files /dev/null and b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png differ http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png ---------------------------------------------------------------------- diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png new file mode 100644 index 0000000..749da14 Binary files /dev/null and b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png differ
