http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/html/package.html ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/html/package.html b/juneau-core/src/main/java/org/apache/juneau/html/package.html new file mode 100644 index 0000000..371c8c4 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/html/package.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<!-- +/*************************************************************************************************************************** + * 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. + * + ***************************************************************************************************************************/ + --> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <style type="text/css"> + /* For viewing in Page Designer */ + @IMPORT url("../../../../../../javadoc.css"); + + /* For viewing in REST interface */ + @IMPORT url("../htdocs/javadoc.css"); + body { + margin: 20px; + } + </style> + <script> + /* Replace all @code and @link tags. */ + window.onload = function() { + document.body.innerHTML = document.body.innerHTML.replace(/\{\@code ([^\}]+)\}/g, '<code>$1</code>'); + document.body.innerHTML = document.body.innerHTML.replace(/\{\@link (([^\}]+)\.)?([^\.\}]+)\}/g, '<code>$3</code>'); + } + </script> +</head> +<body> +<p>HTML serialization and parsing support</p> +<script> + function toggle(x) { + var div = x.nextSibling; + while (div != null && div.nodeType != 1) + div = div.nextSibling; + if (div != null) { + var d = div.style.display; + if (d == 'block' || d == '') { + div.style.display = 'none'; + x.className += " closed"; + } else { + div.style.display = 'block'; + x.className = x.className.replace(/(?:^|\s)closed(?!\S)/g , '' ); + } + } + } +</script> + +<a id='TOC'></a><h5 class='toc'>Table of Contents</h5> +<ol class='toc'> + <li><p><a class='doclink' href='#HtmlSerializer'>HTML serialization support</a></p> + <li><p><a class='doclink' href='#HtmlParser'>HTML parsing support</a></p> +</ol> + +<!-- ======================================================================================================== --> +<a id="HtmlSerializer"></a> +<h2 class='topic' onclick='toggle(this)'>1 - HTML serialization support</h2> +<div class='topic'> + TODO +</div> + +<!-- ======================================================================================================== --> +<a id="HtmlParser"></a> +<h2 class='topic' onclick='toggle(this)'>2 - HTML parsing support</h2> +<div class='topic'> + TODO +</div> + +</body> +</html> \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFile.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFile.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFile.java new file mode 100644 index 0000000..08e78ec --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFile.java @@ -0,0 +1,766 @@ +/*************************************************************************************************************************** + * 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 java.lang.reflect.Modifier.*; +import static org.apache.juneau.ini.ConfigFileFormat.*; +import static org.apache.juneau.ini.ConfigUtils.*; +import static org.apache.juneau.internal.ThrowableUtils.*; + +import java.beans.*; +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +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.*; + +/** + * Implements the API for accessing the contents of a config file. + * <p> + * Refer to {@link org.apache.juneau.ini} for more information. + * + * @author James Bognar ([email protected]) + */ +public abstract class ConfigFile implements Map<String,Section> { + + //-------------------------------------------------------------------------------- + // Abstract methods + //-------------------------------------------------------------------------------- + + /** + * Retrieves an entry value from this config file. + * + * @param sectionName The section name. Must not be <jk>null</jk>. + * @param sectionKey The section key. Must not be <jk>null</jk>. + * @return The value, or the default value if the section or value doesn't exist. + */ + public abstract String get(String sectionName, String sectionKey); + + /** + * Sets an entry value in this config file. + * + * @param sectionName The section name. Must not be <jk>null</jk>. + * @param sectionKey The section key. Must not be <jk>null</jk>. + * @param value The new value. + * @param encoded + * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract String put(String sectionName, String sectionKey, Object value, boolean encoded); + + /** + * Removes an antry from this config file. + * + * @param sectionName The section name. Must not be <jk>null</jk>. + * @param sectionKey The section key. Must not be <jk>null</jk>. + * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract String remove(String sectionName, String sectionKey); + + /** + * Returns the current set of keys in the specified section. + * + * @param sectionName The section name. Must not be <jk>null</jk>. + * @return The list of keys in the specified section, or <jk>null</jk> if section does not exist. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract Set<String> getSectionKeys(String sectionName); + + /** + * Reloads ths config file object from the persisted file contents if the modified timestamp on the file has changed. + * + * @return This object (for method chaining). + * @throws IOException If file could not be read, or file is not associated with this object. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile loadIfModified() throws IOException; + + /** + * Loads ths config file object from the persisted file contents. + * + * @return This object (for method chaining). + * @throws IOException If file could not be read, or file is not associated with this object. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile load() throws IOException; + + /** + * Loads ths config file object from the specified reader. + * + * @param r The reader to read from. + * @return This object (for method chaining). + * @throws IOException If file could not be read, or file is not associated with this object. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile load(Reader r) throws IOException; + + /** + * Adds arbitrary lines to the specified config file section. + * <p> + * The lines can be any of the following.... + * <ul class='spaced-list'> + * <li><js>"# comment"</js> - A comment line. + * <li><js>"key=val"</js> - A key/value pair (equivalent to calling {@link #put(String,Object)}. + * <li><js>" foobar "</js> - Anything else (interpreted as a comment). + * </ul> + * <p> + * If the section does not exist, it will automatically be created. + * + * @param section The name of the section to add lines to, or <jk>null</jk> to add to the beginning unnamed section. + * @param lines The lines to add to the section. + * @return This object (for method chaining). + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile addLines(String section, String...lines); + + /** + * Adds header comments to the specified section. + * <p> + * Header comments are defined as lines that start with <jk>"#"</jk> immediately preceding a section header <jk>"[section]"</jk>. + * These are handled as part of the section itself instead of being interpreted as comments in the previous section. + * <p> + * Header comments can be of the following formats... + * <ul class='spaced-list'> + * <li><js>"# comment"</js> - A comment line. + * <li><js>"comment"</js> - Anything else (will automatically be prefixed with <js>"# "</js>). + * </ul> + * <p> + * If the section does not exist, it will automatically be created. + * + * @param section The name of the section to add lines to, or <jk>null</jk> to add to the default section. + * @param headerComments The comment lines to add to the section. + * @return This object (for method chaining). + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile addHeaderComments(String section, String...headerComments); + + /** + * Removes any header comments from the specified section. + * + * @param section The name of the section to remove header comments from. + * @return This object (for method chaining). + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile clearHeaderComments(String section); + + /** + * Returns the serializer in use for this config file. + * + * @return This object (for method chaining). + * @throws SerializeException If no serializer is defined on this config file. + */ + protected abstract WriterSerializer getSerializer() throws SerializeException; + + /** + * Returns the parser in use for this config file. + * + * @return This object (for method chaining). + * @throws ParseException If no parser is defined on this config file. + */ + protected abstract ReaderParser getParser() throws ParseException; + + /** + * Places a read lock on this config file. + */ + protected abstract void readLock(); + + /** + * Removes the read lock on this config file. + */ + protected abstract void readUnlock(); + + + //-------------------------------------------------------------------------------- + // API methods + //-------------------------------------------------------------------------------- + + /** + * Returns the specified value as a string from the config file. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param def The default value if the section or value does not exist. + * @return The value, or the default value if the section or value doesn't exist. + */ + public final String getString(String key, String def) { + assertFieldNotNull(key, "key"); + String s = get(getSectionName(key), getSectionKey(key)); + return (StringUtils.isEmpty(s) && def != null ? def : s); + } + + /** + * Removes an entry with the specified key. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. + * @throws UnsupportedOperationException If config file is read only. + */ + public final String removeString(String key) { + assertFieldNotNull(key, "key"); + return remove(getSectionName(key), getSectionKey(key)); + } + + /** + * Gets the entry with the specified key and converts it to the specified value. + * <p> + * The key can be in one of the following formats... + * <ul class='spaced-list'> + * <li><js>"key"</js> - A value in the default section (i.e. defined above any <code>[section]</code> header). + * <li><js>"section/key"</js> - A value from the specified section. + * </ul> + * <p> + * If the class type is an array, the value is split on commas and converted individually. + * <p> + * If you specify a primitive element type using this method (e.g. <code><jk>int</jk>.<jk>class</jk></code>, + * you will get an array of wrapped objects (e.g. <code>Integer[].<jk>class</jk></code>. + * + * @param c The class to convert the value to. + * @param key The key. See {@link #getString(String)} for a description of the key. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or <jk>null</jk> if the section or key does not exist. + */ + @SuppressWarnings("unchecked") + public final <T> T getObject(Class<T> c, String key) throws ParseException { + assertFieldNotNull(c, "c"); + return getObject(c, key, c.isArray() ? (T)Array.newInstance(c.getComponentType(), 0) : null); + } + + /** + * Gets the entry with the specified key and converts it to the specified value.. + * <p> + * The key can be in one of the following formats... + * <ul class='spaced-list'> + * <li><js>"key"</js> - A value in the default section (i.e. defined above any <code>[section]</code> header). + * <li><js>"section/key"</js> - A value from the specified section. + * </ul> + * + * @param c The class to convert the value to. + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param def The default value if section or key does not exist. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or <jk>null</jk> if the section or key does not exist. + */ + public final <T> T getObject(Class<T> c, String key, T def) throws ParseException { + assertFieldNotNull(c, "c"); + assertFieldNotNull(key, "key"); + return getObject(c, getSectionName(key), getSectionKey(key), def); + } + + /** + * Same as {@link #getObject(Class, String, Object)}, but value is referenced through section name and key instead of full key. + * + * @param c The class to convert the value to. + * @param sectionName The section name. Must not be <jk>null</jk>. + * @param sectionKey The section key. Must not be <jk>null</jk>. + * @param def The default value if section or key does not exist. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or the default value if the section or value doesn't exist. + */ + @SuppressWarnings("unchecked") + public <T> T getObject(Class<T> c, String sectionName, String sectionKey, T def) throws ParseException { + String s = get(sectionName, sectionKey); + if (s == null) + return def; + if (c == String.class) + return (T)s; + if (c == Integer.class || c == int.class) + return (T)(StringUtils.isEmpty(s) ? def : Integer.valueOf(parseIntWithSuffix(s))); + if (c == Boolean.class || c == boolean.class) + return (T)(StringUtils.isEmpty(s) ? def : Boolean.valueOf(Boolean.parseBoolean(s))); + if (c == String[].class) { + String[] r = StringUtils.isEmpty(s) ? new String[0] : StringUtils.split(s, ','); + return (T)(r.length == 0 ? def : r); + } + if (c.isArray()) { + Class<?> ce = c.getComponentType(); + if (StringUtils.isEmpty(s)) + return def; + String[] r = StringUtils.split(s, ','); + Object o = Array.newInstance(ce, r.length); + for (int i = 0; i < r.length; i++) + Array.set(o, i, getParser().parse(r[i], ce)); + return (T)o; + } + if (StringUtils.isEmpty(s)) + return def; + return getParser().parse(s, c); + } + + /** + * Gets the entry with the specified key. + * <p> + * The key can be in one of the following formats... + * <ul class='spaced-list'> + * <li><js>"key"</js> - A value in the default section (i.e. defined above any <code>[section]</code> header). + * <li><js>"section/key"</js> - A value from the specified section. + * </ul> + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return The value, or <jk>null</jk> if the section or key does not exist. + */ + public final String getString(String key) { + return getString(key, null); + } + + /** + * Gets the entry with the specified key, splits the value on commas, and returns the values as trimmed strings. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return The value, or an empty list if the section or key does not exist. + */ + public final String[] getStringArray(String key) { + return getStringArray(key, new String[0]); + } + + /** + * Same as {@link #getStringArray(String)} but returns a default value if the value cannot be found. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param def The default value if section or key does not exist. + * @return The value, or an empty list if the section or key does not exist. + */ + public final String[] getStringArray(String key, String[] def) { + String s = getString(key); + if (s == null) + return def; + String[] r = StringUtils.isEmpty(s) ? new String[0] : StringUtils.split(s, ','); + return r.length == 0 ? def : r; + } + + /** + * Convenience method for getting int config values. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return The value, or <code>0</code> if the section or key does not exist or cannot be parsed as an integer. + */ + public final int getInt(String key) { + return getInt(key, 0); + } + + /** + * Convenience method for getting int config values. + * <p> + * <js>"M"</js> and <js>"K"</js> can be used to identify millions and thousands. + * + * <dl> + * <dt>Example:</dt> + * <dd> + * <ul class='spaced-list'> + * <li><code><js>"100K"</js> => 1024000</code> + * <li><code><js>"100M"</js> => 104857600</code> + * </ul> + * </dd> + * </dl> + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param def The default value if config file or value does not exist. + * @return The value, or the default value if the section or key does not exist or cannot be parsed as an integer. + */ + public final int getInt(String key, int def) { + String s = getString(key); + if (StringUtils.isEmpty(s)) + return def; + return parseIntWithSuffix(s); + } + + /** + * Convenience method for getting boolean config values. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return The value, or <jk>false</jk> if the section or key does not exist or cannot be parsed as a boolean. + */ + public final boolean getBoolean(String key) { + return getBoolean(key, false); + } + + /** + * Convenience method for getting boolean config values. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param def The default value if config file or value does not exist. + * @return The value, or the default value if the section or key does not exist or cannot be parsed as a boolean. + */ + public final boolean getBoolean(String key, boolean def) { + String s = getString(key); + return StringUtils.isEmpty(s) ? def : Boolean.parseBoolean(s); + } + + /** + * Adds or replaces an entry with the specified key with a POJO serialized to a string using the registered serializer. + * <p> + * Equivalent to calling <code>put(key, value, isEncoded(key))</code>. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param value The new value POJO. + * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. + * @throws SerializeException If serializer could not serialize the value or if a serializer is not registered with this config file. + * @throws UnsupportedOperationException If config file is read only. + */ + public final String put(String key, Object value) throws SerializeException { + return put(key, value, isEncoded(key)); + } + + /** + * Adds or replaces an entry with the specified key with the specified value. + * <p> + * The format of the entry depends on the data type of the value. + * <ul class='spaced-list'> + * <li>Simple types (<code>String</code>, <code>Number</code>, <code>Boolean</code>, primitives) + * are serialized as plain strings. + * <li>Arrays and collections of simple types are serialized as comma-delimited lists of plain strings. + * <li>Other types (e.g. beans) are serialized using the serializer registered with this config file. + * <li>Arrays and collections of other types are serialized as comma-delimited lists of serialized strings of each entry. + * </ul> + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param value The new value. + * @param encoded If <jk>true</jk>, value is encoded by the registered encoder when the config file is persisted to disk. + * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. + * @throws SerializeException If serializer could not serialize the value or if a serializer is not registered with this config file. + * @throws UnsupportedOperationException If config file is read only. + */ + public final String put(String key, Object value, boolean encoded) throws SerializeException { + assertFieldNotNull(key, "key"); + if (value == null) + value = ""; + Class<?> c = value.getClass(); + if (isSimpleType(c)) + return put(getSectionName(key), getSectionKey(key), value.toString(), encoded); + if (c.isAssignableFrom(Collection.class)) { + Collection<?> c2 = (Collection<?>)value; + String[] r = new String[c2.size()]; + int i = 0; + for (Object o2 : c2) { + boolean isSimpleType = o2 == null ? true : isSimpleType(o2.getClass()); + r[i++] = (isSimpleType ? Array.get(value, i).toString() : getSerializer().toString(Array.get(value, i))); + } + return put(getSectionName(key), getSectionKey(key), StringUtils.join(r, ','), encoded); + } else if (c.isArray()) { + boolean isSimpleType = isSimpleType(c.getComponentType()); + String[] r = new String[Array.getLength(value)]; + for (int i = 0; i < r.length; i++) { + r[i] = (isSimpleType ? Array.get(value, i).toString() : getSerializer().toString(Array.get(value, i))); + } + return put(getSectionName(key), getSectionKey(key), StringUtils.join(r, ','), encoded); + } + return put(getSectionName(key), getSectionKey(key), getSerializer().toString(value), encoded); + } + + private final boolean isSimpleType(Class<?> c) { + return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class); + } + + /** + * Returns the specified section as a map of key/value pairs. + * + * @param sectionName The section name to retrieve. + * @return A map of the section, or <jk>null</jk> if the section was not found. + */ + public final ObjectMap getSectionMap(String sectionName) { + readLock(); + try { + Set<String> keys = getSectionKeys(sectionName); + if (keys == null) + return null; + ObjectMap m = new ObjectMap(); + for (String key : keys) + m.put(key, get(sectionName, key)); + return m; + } finally { + readUnlock(); + } + } + + /** + * Copies the entries in a section to the specified bean by calling the public setters on that bean. + * + * @param sectionName The section name to write from. + * @param bean The bean to set the properties on. + * @param ignoreUnknownProperties If <jk>true</jk>, don't throw an {@link IllegalArgumentException} if this section + * contains a key that doesn't correspond to a setter method. + * @param permittedPropertyTypes If specified, only look for setters whose property types + * are those listed. If not specified, use all setters. + * @return An object map of the changes made to the bean. + * @throws ParseException If parser was not set on this config file or invalid properties were found in the section. + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + public final ObjectMap writeProperties(String sectionName, Object bean, boolean ignoreUnknownProperties, Class<?>...permittedPropertyTypes) throws ParseException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { + assertFieldNotNull(bean, "bean"); + ObjectMap om = new ObjectMap(); + readLock(); + try { + Set<String> keys = getSectionKeys(sectionName); + if (keys == null) + throw new IllegalArgumentException("Section not found"); + keys = new LinkedHashSet<String>(keys); + for (Method m : bean.getClass().getMethods()) { + int mod = m.getModifiers(); + if (isPublic(mod) && (!isStatic(mod)) && m.getName().startsWith("set") && m.getParameterTypes().length == 1) { + Class<?> pt = m.getParameterTypes()[0]; + if (permittedPropertyTypes == null || permittedPropertyTypes.length == 0 || ArrayUtils.contains(pt, permittedPropertyTypes)) { + String propName = Introspector.decapitalize(m.getName().substring(3)); + Object value = getObject(pt, sectionName, propName, null); + if (value != null) { + m.invoke(bean, value); + om.put(propName, value); + keys.remove(propName); + } + } + } + } + if (! (ignoreUnknownProperties || keys.isEmpty())) + throw new ParseException("Invalid properties found in config file section ["+sectionName+"]: " + JsonSerializer.DEFAULT_LAX.toString(keys)); + return om; + } finally { + readUnlock(); + } + } + + /** + * Shortcut for calling <code>asBean(sectionName, c, <jk>false</jk>)</code>. + * + * @param sectionName The section name to write from. + * @param c The bean class to create. + * @return A new bean instance. + * @throws ParseException + */ + public final <T> T getSectionAsBean(String sectionName, Class<T>c) throws ParseException { + return getSectionAsBean(sectionName, c, false); + } + + /** + * Converts this config file section to the specified bean instance. + * + * @param sectionName The section name to write from. + * @param c The bean class to create. + * @param ignoreUnknownProperties If <jk>false</jk>, throws a {@link ParseException} if + * the section contains an entry that isn't a bean property name. + * @return A new bean instance. + * @throws ParseException + */ + public final <T> T getSectionAsBean(String sectionName, Class<T> c, boolean ignoreUnknownProperties) throws ParseException { + assertFieldNotNull(c, "c"); + readLock(); + try { + BeanMap<T> bm = getParser().getBeanContext().newBeanMap(c); + for (String k : getSectionKeys(sectionName)) { + BeanPropertyMeta<?> bpm = bm.getPropertyMeta(k); + if (bpm == null) { + if (! ignoreUnknownProperties) + throw new ParseException("Unknown property {0} encountered", k); + } else { + bm.put(k, getObject(bpm.getClassMeta().getInnerClass(), sectionName + '/' + k)); + } + } + return bm.getBean(); + } finally { + readUnlock(); + } + } + + /** + * Returns <jk>true</jk> if this section contains the specified key and the key has a non-blank value. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return <jk>true</jk> if this section contains the specified key and the key has a non-blank value. + */ + public final boolean containsNonEmptyValue(String key) { + return ! StringUtils.isEmpty(getString(key, null)); + } + + /** + * Gets the section with the specified name. + * + * @param name The section name. + * @return The section, or <jk>null</jk> if section does not exist. + */ + protected abstract Section getSection(String name); + + /** + * Gets the section with the specified name and optionally creates it if it's not there. + * + * @param name The section name. + * @param create Create the section if it's not there. + * @return The section, or <jk>null</jk> if section does not exist. + * @throws UnsupportedOperationException If config file is read only and section doesn't exist and <code>create</code> is <jk>true</jk>. + */ + protected abstract Section getSection(String name, boolean create); + + /** + * Appends a section to this config file if it does not already exist. + * <p> + * Returns the existing section if it already exists. + * + * @param name The section name, or <jk>null</jk> for the default section. + * @return The appended or existing section. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile addSection(String name); + + /** + * Creates or overwrites the specified section. + * + * @param name The section name, or <jk>null</jk> for the default section. + * @param contents The contents of the new section. + * @return The appended or existing section. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile setSection(String name, Map<String,String> contents); + + /** + * Removes the section with the specified name. + * + * @param name The name of the section to remove, or <jk>null</jk> for the default section. + * @return The removed section, or <jk>null</jk> if named section does not exist. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile removeSection(String name); + + /** + * Returns <jk>true</jk> if the encoding flag is set on the specified entry. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return <jk>true</jk> if the encoding flag is set on the specified entry. + */ + public abstract boolean isEncoded(String key); + + /** + * Saves this config file to disk. + * + * @return This object (for method chaining). + * @throws IOException If a problem occurred trying to save file to disk, or file is not associated with this object. + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile save() throws IOException; + + /** + * Saves this config file to the specified writer as an INI file. + * <p> + * The writer will automatically be closed. + * + * @param out The writer to send the output to. + * @return This object (for method chaining). + * @throws IOException If a problem occurred trying to send contents to the writer. + */ + public final ConfigFile serializeTo(Writer out) throws IOException { + return serializeTo(out, INI); + } + + /** + * Same as {@link #serializeTo(Writer)}, except allows you to explicitely specify a format. + * + * @param out The writer to send the output to. + * @param format The {@link ConfigFileFormat} of the output. + * @return This object (for method chaining). + * @throws IOException If a problem occurred trying to send contents to the writer. + */ + public abstract ConfigFile serializeTo(Writer out, ConfigFileFormat format) throws IOException; + + /** + * Add a listener to this config file to react to modification events. + * + * @param listener The new listener to add. + * @return This object (for method chaining). + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile addListener(ConfigFileListener listener); + + /** + * Merges the contents of the specified config file into this config file. + * <p> + * Pretty much identical to just replacing this config file, but + * causes the {@link ConfigFileListener#onChange(ConfigFile, Set)} method to be invoked + * on differences between the file. + * @param cf The config file whose values should be copied into this config file. + * @return This object (for method chaining). + * @throws UnsupportedOperationException If config file is read only. + */ + public abstract ConfigFile merge(ConfigFile cf); + + /** + * Returns the config file contents as a string. + * <p> + * The contents of the string are the same as the contents that would be serialized to disk. + */ + @Override /* Object */ + public abstract String toString(); + + /** + * Returns a wrapped instance of this config file where calls to getters + * have their values first resolved by the specified {@link VarResolver}. + * + * @param vr The {@link VarResolver} for resolving variables in values. + * @return This config file wrapped in an instance of {@link ConfigFileWrapped}. + */ + public abstract ConfigFile getResolving(VarResolver vr); + + /** + * Returns a wrapped instance of this config file where calls to getters + * have their values first resolved by the specified {@link VarResolverSession}. + * + * @param vs The {@link VarResolverSession} for resolving variables in values. + * @return This config file wrapped in an instance of {@link ConfigFileWrapped}. + */ + public abstract ConfigFile getResolving(VarResolverSession vs); + + /** + * Returns a wrapped instance of this config file where calls to getters have their values + * first resolved by a default {@link VarResolver}. + * + * The default {@link VarResolver} is registered with the following {@link Var StringVars}: + * <ul class='spaced-list'> + * <li><code>$S{key}</code>,<code>$S{key,default}</code> - System properties. + * <li><code>$E{key}</code>,<code>$E{key,default}</code> - Environment variables. + * <li><code>$C{key}</code>,<code>$C{key,default}</code> - Values in this configuration file. + * </ul> + * + * @return A new config file that resolves string variables. + */ + public abstract ConfigFile getResolving(); + + /** + * Wraps this config file in a {@link Writable} interface that renders it as plain text. + * + * @return This config file wrapped in a {@link Writable}. + */ + public abstract Writable toWritable(); + + /** + * @return The string var resolver associated with this config file. + */ + protected VarResolver getVarResolver() { + // Only ConfigFileWrapped returns a value. + return null; + } + + + private int parseIntWithSuffix(String s) { + assertFieldNotNull(s, "s"); + int m = 1; + if (s.endsWith("M")) { + m = 1024*1024; + s = s.substring(0, s.length()-1).trim(); + } else if (s.endsWith("K")) { + m = 1024; + s = s.substring(0, s.length()-1).trim(); + } + return Integer.parseInt(s) * m; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java new file mode 100644 index 0000000..821f7d3 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java @@ -0,0 +1,29 @@ +/*************************************************************************************************************************** + * 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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java new file mode 100644 index 0000000..4aaaa46 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java @@ -0,0 +1,747 @@ +/*************************************************************************************************************************** + * 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 java.io.*; +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}. + * + * @author James Bognar ([email protected]) + */ +public final class ConfigFileImpl extends ConfigFile { + + private final File file; + private final Encoder encoder; + private final WriterSerializer serializer; + private final ReaderParser parser; + 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(Class,String)} 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(); + } + } + + /** + * Constructor. + * 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. + * 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 = StringUtils.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; + } + + //-------------------------------------------------------------------------------- + // 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()); + } + + @Override /* ConfigFile */ + public String put(String sectionName, String sectionKey, Object value, boolean encoded) { + assertFieldNotNull(sectionKey, "sectionKey"); + Section s = getSection(sectionName, true); + return s.put(sectionKey, value.toString(), 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 WriterSerializer getSerializer() throws SerializeException { + if (serializer == null) + throw new SerializeException("Serializer not defined on config file."); + return serializer; + } + + @Override /* ConfigFile */ + protected ReaderParser getParser() throws ParseException { + if (parser == null) + throw new ParseException("Parser not defined on config file."); + return parser; + } + + @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(VarResolver.DEFAULT.clone().addVars(ConfigFileVar.class).setContextObject(ConfigFileVar.SESSION_config, this)); + } + + /* + * Finds the keys that are different between the two sections and adds it to + * the specified set. + */ + private 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 void addChange(Set<String> changes, String section, String key, String oldVal, String newVal) { + if (! StringUtils.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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileListener.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileListener.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileListener.java new file mode 100644 index 0000000..3a1d37a --- /dev/null +++ b/juneau-core/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/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java new file mode 100644 index 0000000..c9175d4 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java @@ -0,0 +1,278 @@ +/*************************************************************************************************************************** + * 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.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; +import org.apache.juneau.svl.vars.*; + +/** + * 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. + * + * @author James Bognar ([email protected]) + */ +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.clone() + .addVars(ConfigFileVar.class) + .setContextObject(ConfigFileVar.SESSION_config, cf) + .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 WriterSerializer getSerializer() throws SerializeException { + return cf.getSerializer(); + } + + @Override /* ConfigFile */ + protected ReaderParser getParser() throws ParseException { + return cf.getParser(); + } + + @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, Object value, boolean encoded) { + return cf.put(sectionName, sectionKey, value, encoded); + } + + @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(); + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java new file mode 100644 index 0000000..1d076fa --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java @@ -0,0 +1,44 @@ +/*************************************************************************************************************************** + * 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.*; + +/** + * 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 String getMediaType() { + return "text/plain"; + } +} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/e6bf97a8/juneau-core/src/main/java/org/apache/juneau/ini/ConfigMgr.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigMgr.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigMgr.java new file mode 100644 index 0000000..64c19b7 --- /dev/null +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigMgr.java @@ -0,0 +1,314 @@ +/*************************************************************************************************************************** + * 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 java.io.*; +import java.nio.charset.*; +import java.util.*; +import java.util.concurrent.*; + +import org.apache.juneau.internal.*; +import org.apache.juneau.json.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.utils.*; + +/** + * Manager for retrieving shared instances of {@link ConfigFile ConfigFiles}. + * <p> + * Example: + * <p class='bcode'> + * ConfigFile cf = ConfigMgr.<jsf>DEFAULT</jsf>.get(<js>"MyConfig.cfg"</js>); + * String setting = cf.get(<js>"MySection/mysetting"</js>); + * </p> + */ +public class ConfigMgr { + + /** + * Default reusable configuration manager. + * <ul class='spaced-list'> + * <li>Read-only: <jk>false</jk>. + * <li>Encoder: {@link XorEncoder}. + * <li>Serializer: {@link JsonSerializer#DEFAULT}. + * <li>Parser: {@link JsonParser#DEFAULT}. + * <li>Charset: {@link Charset#defaultCharset()}. + * <li>Search paths: [<js>"."</js>]. + * </ul> + */ + public static final ConfigMgr DEFAULT = new ConfigMgr(false, new XorEncoder(), JsonSerializer.DEFAULT, JsonParser.DEFAULT, Charset.defaultCharset(), new String[]{"."}); + + private ConcurrentHashMap<String,File> files = new ConcurrentHashMap<String,File>(); + private ConcurrentHashMap<File,ConfigFile> configs = new ConcurrentHashMap<File,ConfigFile>(); + private final WriterSerializer serializer; + private final ReaderParser parser; + private final Encoder encoder; + private final boolean readOnly; + private final Charset charset; + private final List<File> searchPaths = new LinkedList<File>(); + + /** + * Create a custom configuration manager. + * + * @param readOnly Make {@link ConfigFile ConfigFiles} read-only. + * @param encoder Optional. Specify the encoder to use for encoded config file entries (e.g. <js>"mySecret*={...}"</js>). + * @param serializer Optional. Specify the serializer to use for serializing POJOs when using {@link ConfigFile#put(String, Object)}. + * @param parser Optional. Specify the parser to use for parsing POJOs when using {@link ConfigFile#getObject(Class,String)}. + * @param charset Optional. Specify the config file character encoding. If <jk>null</jk>, uses {@link Charset#defaultCharset()}. + * @param searchPaths Specify the search paths for config files. Can contain relative or absolute paths. + */ + public ConfigMgr(boolean readOnly, Encoder encoder, WriterSerializer serializer, ReaderParser parser, Charset charset, String[] searchPaths) { + this.readOnly = readOnly; + this.encoder = encoder; + this.serializer = serializer; + this.parser = parser; + this.charset = charset; + if (searchPaths != null) + for (String p : searchPaths) + this.searchPaths.add(new File(p)); + } + + /** + * Returns the config file with the specified absolute or relative path. + * <p> + * Multiple calls to the same path return the same <code>ConfigFile</code> instance. + * + * @param path The absolute or relative path of the config file. + * @return The config file. + * @throws IOException If config file could not be parsed. + * @throws FileNotFoundException If config file could not be found. + */ + public ConfigFile get(String path) throws IOException { + return get(path, false); + } + + /** + * Returns the config file with the specified absolute or relative path. + * <p> + * Multiple calls to the same path return the same <code>ConfigFile</code> instance. + * <p> + * If file doesn't exist and <code>create</code> is <jk>true</jk>, the configuration file will be + * create in the location identified by the first entry in the search paths. + * + * @param path The absolute or relative path of the config file. + * @param create Create the config file if it doesn't exist. + * @return The config file. + * @throws IOException If config file could not be parsed. + * @throws FileNotFoundException If config file could not be found or could not be created. + */ + public ConfigFile get(String path, boolean create) throws IOException { + + File f = resolve(path, create); + + ConfigFile cf = configs.get(f); + if (cf != null) + return cf; + + cf = new ConfigFileImpl(f, readOnly, encoder, serializer, parser, charset); + configs.putIfAbsent(f, cf); + return configs.get(f); + } + + /** + * Create a new empty config file not backed by any file. + * + * @return A new config file. + * @throws IOException + */ + public ConfigFile create() throws IOException { + return new ConfigFileImpl(null, false, encoder, serializer, parser, charset); + } + + /** + * Create a new config file backed by the specified file. + * Note that {@link #get(String)} is the preferred method for getting access to config files + * since this method will create a new config file each time it is called. + * This method is provided primarily for testing purposes. + * + * @param f The file to create a config file from. + * @return A new config file. + * @throws IOException + */ + public ConfigFile create(File f) throws IOException { + return new ConfigFileImpl(f, false, encoder, serializer, parser, charset); + } + + /** + * Create a new config file not backed by a file. + * + * @param r The reader containing an INI-formatted file to initialize the config file from. + * @return A new config file. + * @throws IOException + */ + public ConfigFile create(Reader r) throws IOException { + return new ConfigFileImpl(null, false, encoder, serializer, parser, charset).load(r); + } + + /** + * Reloads any config files that were modified. + * @throws IOException + */ + public void loadIfModified() throws IOException { + for (ConfigFile cf : configs.values()) + cf.loadIfModified(); + } + + /** + * Delete all configuration files registered with this config manager. + */ + public void deleteAll() { + for (File f : configs.keySet()) + FileUtils.delete(f); + files.clear(); + configs.clear(); + } + + private File resolve(String path, boolean create) throws IOException { + + // See if it's cached. + File f = files.get(path); + if (f != null) + return f; + + // Handle absolute file. + f = new File(path); + if (f.isAbsolute()) { + if (create) + FileUtils.create(f); + if (f.exists()) + return addFile(path, f); + throw new FileNotFoundException("Could not find config file '"+path+"'"); + } + + if (searchPaths.isEmpty()) + throw new FileNotFoundException("No search paths specified on ConfigMgr."); + + // Handle paths relative to search paths. + for (File sf : searchPaths) { + f = new File(sf.getAbsolutePath() + "/" + path); + if (f.exists()) + return addFile(path, f); + } + + if (create) { + f = new File(searchPaths.get(0).getAbsolutePath() + "/" + path); + FileUtils.create(f); + return addFile(path, f); + } + + throw new FileNotFoundException("Could not find config file '"+path+"'"); + } + + private File addFile(String path, File f) { + files.putIfAbsent(path, f); + return files.get(path); + } + + /** + * Implements command-line features for working with INI configuration files. + * <p> + * Invoke as a normal Java program... + * <p> + * <p class='bcode'> + * java org.apache.juneau.ini.ConfigMgr [args] + * </p> + * <p> + * Arguments can be any of the following... + * <ul class='spaced-list'> + * <li>No arguments<br> + * Prints usage message.<br> + * <li><code>createBatchEnvFile -configfile <configFile> -envfile <batchFile> [-verbose]</code><br> + * Creates a batch file that will set each config file entry as an environment variable.<br> + * Characters in the keys that are not valid as environment variable names (e.g. <js>'/'</js> and <js>'.'</js>) + * will be converted to underscores.<br> + * <li><code>createShellEnvFile -configFile <configFile> -envFile <configFile> [-verbose]</code> + * Creates a shell script that will set each config file entry as an environment variable.<br> + * Characters in the keys that are not valid as environment variable names (e.g. <js>'/'</js> and <js>'.'</js>) + * will be converted to underscores.<br> + * <li><code>setVals -configFile <configFile> -vals [var1=val1 [var2=val2...]] [-verbose]</code> + * Sets values in config files.<br> + * </ul> + * <p> + * For example, the following command will create the file <code>'MyConfig.bat'</code> from the contents of the file <code>'MyConfig.cfg'</code>. + * <p class='bcode'> + * java org.apache.juneau.ini.ConfigMgr createBatchEnvFile -configfile C:\foo\MyConfig.cfg -batchfile C:\foo\MyConfig.bat + * </p> + * + * @param args Command-line arguments + */ + public static void main(String[] args) { + + Args a = new Args(args); + String command = a.getArg(0); + String configFile = a.getArg("configFile"); + String envFile = a.getArg("envFile"); + List<String> vals = a.getArgs("vals"); + + if (command == null || ! (command.equals("createBatchEnvFile") || command.equals("createShellEnvFile") || command.equals("setVals"))) + printUsageAndExit(); + else if (configFile.isEmpty()) + printUsageAndExit(); + else if (command.equals("setVals") && vals.isEmpty()) + printUsageAndExit(); + else if ((command.equals("createBatchEnvFile") || command.equals("createShellEnvFile")) && envFile.isEmpty()) + printUsageAndExit(); + else { + try { + ConfigFile cf = ConfigMgr.DEFAULT.get(configFile); + + if (command.equalsIgnoreCase("setVals")) { + for (String val : vals) { + String[] x = val.split("\\="); + if (x.length != 2) + throw new RuntimeException("Invalid format for value: '"+val+"'. Must be in the format 'key=value'"); + cf.put(x[0], x[1]); + } + cf.save(); + return; + + } else if (command.equalsIgnoreCase("createBatchEnvFile")) { + Writer fw = new OutputStreamWriter(new FileOutputStream(envFile), Charset.defaultCharset()); + try { + cf.serializeTo(fw, BATCH); + } finally { + fw.close(); + } + return; + + } else if (command.equalsIgnoreCase("createShellEnvFile")) { + Writer fw = new OutputStreamWriter(new FileOutputStream(envFile), Charset.defaultCharset()); + try { + cf.serializeTo(fw, SHELL); + } finally { + fw.close(); + } + return; + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private static void printUsageAndExit() { + System.err.println("---Usage---"); + System.err.println("java -cp juneau.jar org.apache.juneau.ini.ConfigFile createBatchEnvFile -configFile <configFile> -envFile <envFile> [-verbose]"); + System.err.println("java -cp juneau.jar org.apache.juneau.ini.ConfigFile createShellEnvFile -configFile <configFile> -envFile <envFile> [-verbose]"); + System.err.println("java -cp juneau.jar org.apache.juneau.ini.ConfigFile setVals -configFile <configFile> -vals [var1 val1 [var2 val2...]] [-verbose]"); + int rc = Integer.getInteger("exit.2", 2); + if (rc != 0) + System.exit(rc); + } +}
