From commits-return-5108-archive-asf-public=cust-asf.ponee.io@juneau.apache.org Wed Feb 21 03:38:15 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id E3B16180654 for ; Wed, 21 Feb 2018 03:38:13 +0100 (CET) Received: (qmail 50801 invoked by uid 500); 21 Feb 2018 02:38:12 -0000 Mailing-List: contact commits-help@juneau.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@juneau.apache.org Delivered-To: mailing list commits@juneau.apache.org Received: (qmail 50788 invoked by uid 99); 21 Feb 2018 02:38:11 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 21 Feb 2018 02:38:11 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 9BE7B824C9; Wed, 21 Feb 2018 02:38:09 +0000 (UTC) Date: Wed, 21 Feb 2018 02:38:09 +0000 To: "commits@juneau.apache.org" Subject: [juneau] branch master updated: Config API refactoring. MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Message-ID: <151918068989.15205.17221257660058365647@gitbox.apache.org> From: jamesbognar@apache.org X-Git-Host: gitbox.apache.org X-Git-Repo: juneau X-Git-Refname: refs/heads/master X-Git-Reftype: branch X-Git-Oldrev: a87f1b654d88f2f3bbe2df4aebd648d83f513077 X-Git-Newrev: 8611b246f9a0f5e11f6060197260f0107eef85dd X-Git-Rev: 8611b246f9a0f5e11f6060197260f0107eef85dd X-Git-NotificationType: ref_changed_plus_diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated This is an automated email from the ASF dual-hosted git repository. jamesbognar pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/juneau.git The following commit(s) were added to refs/heads/master by this push: new 8611b24 Config API refactoring. 8611b24 is described below commit 8611b246f9a0f5e11f6060197260f0107eef85dd Author: JamesBognar AuthorDate: Tue Feb 20 21:38:07 2018 -0500 Config API refactoring. --- .../apache/juneau/config/ConfigFileWritable.java | 3 +- .../juneau/config/event/ChangeEventListener.java | 2 +- .../org/apache/juneau/config/proto/Config.java | 1339 ++++++++++++++++++++ .../apache/juneau/config/proto/ConfigBuilder.java | 323 +++++ .../org/apache/juneau/config/proto/ConfigMod.java | 103 ++ .../apache/juneau/config/store/ConfigEntry.java | 31 +- .../org/apache/juneau/config/store/ConfigMap.java | 200 +-- .../juneau/config/proto/ConfigMapListenerTest.java | 62 +- .../src/main/java/org/apache/juneau/Writable.java | 3 +- .../org/apache/juneau/internal/StringUtils.java | 2 + .../org/apache/juneau/utils/StringMessage.java | 4 +- .../java/org/apache/juneau/utils/StringObject.java | 3 +- .../org/apache/juneau/rest/ReaderResource.java | 3 +- 13 files changed, 1943 insertions(+), 135 deletions(-) diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java index f846cb5..f8ee441 100644 --- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java @@ -34,10 +34,11 @@ class ConfigFileWritable implements Writable { } @Override /* Writable */ - public void writeTo(Writer out) throws IOException { + public Writer writeTo(Writer out) throws IOException { cf.readLock(); try { cf.serializeTo(out); + return out; } finally { cf.readUnlock(); } diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java index 546f330..335c66c 100644 --- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java @@ -24,5 +24,5 @@ public interface ChangeEventListener { * * @param events The change events. */ - void onEvents(List events); + void onChange(List events); } diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/Config.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/Config.java new file mode 100644 index 0000000..3b1b30f --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/Config.java @@ -0,0 +1,1339 @@ +// *************************************************************************************************************************** +// * 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.config.proto; + +import static org.apache.juneau.internal.StringUtils.*; +import static org.apache.juneau.internal.ThrowableUtils.*; +import static org.apache.juneau.config.proto.ConfigMod.*; +import static java.lang.reflect.Modifier.*; + +import java.beans.*; +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.config.encode.*; +import org.apache.juneau.config.encode.Encoder; +import org.apache.juneau.config.event.*; +import org.apache.juneau.config.store.*; +import org.apache.juneau.config.vars.*; +import org.apache.juneau.http.*; +import org.apache.juneau.json.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; + +/** + * TODO + */ +public final class Config extends Context implements ChangeEventListener, Closeable, Writable { + + //------------------------------------------------------------------------------------------------------------------- + // Configurable properties + //------------------------------------------------------------------------------------------------------------------- + + private static final String PREFIX = "Config."; + + /** + * Configuration property: Configuration name. + * + *
Property:
+ *
    + *
  • Name: "Config.name.s" + *
  • Data type: String + *
  • Default: "Configuration" + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#name(String)} + *
    + *
+ * + *
Description:
+ *

+ * Specifies the configuration name. + *
This is typically the configuration file name minus the file extension, although + * the name can be anything identifiable by the {@link Store} used for retrieving and storing the configuration. + */ + public static final String CONFIG_name = PREFIX + "name.s"; + + /** + * Configuration property: Configuration store. + * + *

Property:
+ *
    + *
  • Name: "Config.store.o" + *
  • Data type: {@link Store} + *
  • Default: {@link FileStore#DEFAULT} + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#store(Store)} + *
    + *
+ * + *
Description:
+ *

+ * The configuration store used for retrieving and storing configurations. + */ + public static final String CONFIG_store = PREFIX + "store.o"; + + /** + * Configuration property: POJO serializer. + * + *

Property:
+ *
    + *
  • Name: "Config.serializer.o" + *
  • Data type: {@link WriterSerializer} + *
  • Default: {@link JsonSerializer#DEFAULT_LAX} + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#serializer(Class)} + *
    • {@link ConfigBuilder#serializer(WriterSerializer)} + *
    + *
+ * + *
Description:
+ *

+ * The serializer to use for serializing POJO values. + */ + public static final String CONFIG_serializer = PREFIX + "serializer.o"; + + /** + * Configuration property: POJO parser. + * + *

Property:
+ *
    + *
  • Name: "Config.parser.o" + *
  • Data type: {@link ReaderParser} + *
  • Default: {@link JsonParser#DEFAULT} + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#parser(Class)} + *
    • {@link ConfigBuilder#parser(ReaderParser)} + *
    + *
+ * + *
Description:
+ *

+ * The parser to use for parsing values to POJOs. + */ + public static final String CONFIG_parser = PREFIX + "parser.o"; + + /** + * Configuration property: Value encoder. + * + *

Property:
+ *
    + *
  • Name: "Config.encoder.o" + *
  • Data type: {@link Encoder} + *
  • Default: {@link XorEncoder#INSTANCE} + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#encoder(Class)} + *
    • {@link ConfigBuilder#encoder(Encoder)} + *
    + *
+ * + *
Description:
+ *

+ * The encoder to use for encoding encoded configuration values. + */ + public static final String CONFIG_encoder = PREFIX + "encoder.o"; + + /** + * Configuration property: SVL variable resolver. + * + *

Property:
+ *
    + *
  • Name: "Config.varResolver.o" + *
  • Data type: {@link VarResolver} + *
  • Default: {@link VarResolver#DEFAULT} + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#varResolver(Class)} + *
    • {@link ConfigBuilder#varResolver(VarResolver)} + *
    + *
+ * + *
Description:
+ *

+ * The resolver to use for resolving SVL variables. + */ + public static final String CONFIG_varResolver = PREFIX + "varResolver.o"; + + /** + * Configuration property: Binary value line length. + * + *

Property:
+ *
    + *
  • Name: "Config.binaryLineLength.i" + *
  • Data type: Integer + *
  • Default: -1 + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#binaryLineLength(int)} + *
    + *
+ * + *
Description:
+ *

+ * When serializing binary values, lines will be split after this many characters. + *
Use -1 to represent no line splitting. + */ + public static final String CONFIG_binaryLineLength = PREFIX + "binaryLineLength.i"; + + /** + * Configuration property: Binary value format. + * + *

Property:
+ *
    + *
  • Name: "Config.binaryFormat.s" + *
  • Data type: String + *
  • Default: "BASE64" + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#binaryFormat(String)} + *
    + *
+ * + *
Description:
+ *

+ * The format to use when persisting byte arrays. + * + *

+ * Possible values: + *

    + *
  • "BASE64" - BASE64-encoded string. + *
  • "HEX" - Hexadecimal. + *
  • "SPACED_HEX" - Hexadecimal with spaces between bytes. + *
+ */ + public static final String CONFIG_binaryFormat = PREFIX + "binaryFormat.s"; + + /** + * Configuration property: Beans on separate lines. + * + *
Property:
+ *
    + *
  • Name: "Config.beanOnSeparateLines.b" + *
  • Data type: Boolean + *
  • Default: false + *
  • Methods: + *
      + *
    • {@link ConfigBuilder#beansOnSeparateLines(boolean)} + *
    + *
+ * + *
Description:
+ *

+ * When enabled, serialized POJOs will be placed on a separate line from the key. + */ + public static final String CONFIG_beansOnSeparateLines = PREFIX + "beansOnSeparateLines.b"; + + + //------------------------------------------------------------------------------------------------------------------- + // Instance + //------------------------------------------------------------------------------------------------------------------- + + private final String name; + private final Store store; + private final WriterSerializer serializer; + private final ReaderParser parser; + private final Encoder encoder; + private final VarResolverSession varSession; + private final int binaryLineLength; + private final String binaryFormat; + private final boolean beansOnSeparateLines; + private final ConfigMap configMap; + private final BeanSession beanSession; + private volatile boolean closed; + private final List listeners = Collections.synchronizedList(new LinkedList()); + + + /** + * Instantiates a new clean-slate {@link ConfigBuilder} object. + * + *

+ * This is equivalent to simply calling new ConfigBuilder(). + * + * @return A new {@link ConfigBuilder} object. + */ + public static ConfigBuilder create() { + return new ConfigBuilder(); + } + + @Override /* Context */ + public ConfigBuilder builder() { + return new ConfigBuilder(getPropertyStore()); + } + + /** + * Constructor. + * + * @param ps + * The property store containing all the settings for this object. + * @throws IOException + */ + public Config(PropertyStore ps) throws IOException { + super(ps); + + name = getStringProperty(CONFIG_name, "Configuration"); + store = getInstanceProperty(CONFIG_store, Store.class, FileStore.DEFAULT); + configMap = store.getMap(name); + configMap.register(this); + serializer = getInstanceProperty(CONFIG_serializer, WriterSerializer.class, JsonSerializer.DEFAULT_LAX); + parser = getInstanceProperty(CONFIG_parser, ReaderParser.class, JsonParser.DEFAULT); + beanSession = parser.createBeanSession(); + encoder = getInstanceProperty(CONFIG_encoder, Encoder.class, XorEncoder.INSTANCE); + varSession = getInstanceProperty(CONFIG_varResolver, VarResolver.class, VarResolver.DEFAULT) + .builder() + .vars(ConfigFileVar.class) + .contextObject(ConfigFileVar.SESSION_config, this) + .build() + .createSession(); + binaryLineLength = getIntegerProperty(CONFIG_binaryLineLength, -1); + binaryFormat = getStringProperty(CONFIG_binaryFormat, "BASE64").toUpperCase(); + beansOnSeparateLines = getBooleanProperty(CONFIG_beansOnSeparateLines, false); + } + + Config(Config copyFrom, VarResolverSession varSession) { + super(null); + name = copyFrom.name; + store = copyFrom.store; + configMap = copyFrom.configMap; + configMap.register(this); + serializer = copyFrom.serializer; + parser = copyFrom.parser; + encoder = copyFrom.encoder; + this.varSession = varSession; + binaryLineLength = copyFrom.binaryLineLength; + binaryFormat = copyFrom.binaryFormat; + beansOnSeparateLines = copyFrom.beansOnSeparateLines; + beanSession = copyFrom.beanSession; + } + + /** + * Creates a copy of this config using the specified var session for resolving variables. + * + *

+ * This creates a shallow copy of the config but replacing the variable resolver. + * + * @param varSession The var session used for resolving string variables. + * @return A new config object. + */ + public Config resolving(VarResolverSession varSession) { + return new Config(this, varSession); + } + + + //-------------------------------------------------------------------------------- + // Workhorse getters + //-------------------------------------------------------------------------------- + + /** + * Returns the specified value as a string from the config file. + * + *

+ * Unlike {@link #getString(String)}, this method doesn't replace SVL variables. + * + * @param key The key. + * @return The value, or null if the section or value doesn't exist. + */ + public String get(String key) { + + String sname = sname(key); + String skey = skey(key); + ConfigEntry ce = configMap.getEntry(sname, skey); + + if (ce == null || ce.getValue() == null) + return null; + + String val = ce.getValue(); + for (ConfigMod m : ConfigMod.asModifiersReverse(ce.getModifiers())) { + if (m == ENCODED) { + val = encoder.decode(key, val); + } + } + + return val; + } + + + //-------------------------------------------------------------------------------- + // Workhorse setters + //-------------------------------------------------------------------------------- + + /** + * Sets a value in this config. + * + * @param key The key. + * @param value The value. + * @return This object (for method chaining). + */ + public Config set(String key, String value) { + assertFieldNotNull(key, "key"); + String sname = sname(key); + String skey = skey(key); + ConfigEntry ce = configMap.getEntry(sname, skey); + + String s = asString(value); + for (ConfigMod m : ConfigMod.asModifiers(ce.getModifiers())) { + if (m == ENCODED) { + value = encoder.encode(key, s); + } + } + + configMap.setValue(sname, skey, s); + return this; + } + + /** + * Adds or replaces an entry with the specified key with a POJO serialized to a string using the registered + * serializer. + * + *

+ * Equivalent to calling put(key, value, isEncoded(key)). + * + * @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 null 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 Config set(String key, Object value) throws SerializeException { + return set(key, value, null); + } + + /** + * Same as {@link #set(String, Object)} but allows you to specify the serializer to use to serialize the + * value. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param value The new value. + * @param serializer + * The serializer to use for serializing the object. + * If null, then uses the predefined serializer on the config file. + * @return The previous value, or null 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 Config set(String key, Object value, Serializer serializer) throws SerializeException { + return set(key, serialize(value, serializer)); + } + + /** + * Same as {@link #set(String, Object)} but allows you to specify all aspects of a value. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param value The new value. + * @param serializer + * The serializer to use for serializing the object. + * If null, then uses the predefined serializer on the config file. + * @param modifiers + * Optional modifiers to apply to the value. + *
Can be null. + * @param comment + * Optional same-line comment to add to this value. + *
Can be null. + * @param preLines + * Optional comment or blank lines to add before this entry. + *
Can be null. + * @return The previous value, or null 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 Config set(String key, Object value, Serializer serializer, ConfigMod[] modifiers, String comment, List preLines) throws SerializeException { + assertFieldNotNull(key, "key"); + String sname = sname(key); + String skey = skey(key); + ConfigEntry ce = configMap.getEntry(sname, skey); + + String s = serialize(value, serializer); + for (ConfigMod m : ConfigMod.asModifiers(ce.getModifiers())) { + if (m == ENCODED) { + s = encoder.encode(key, s); + } + } + + configMap.setEntry(sname, skey, s, ConfigMod.asString(modifiers), comment, preLines); + return this; + } + + /** + * 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 null if the section or key did not previously exist. + * @throws UnsupportedOperationException If config file is read only. + */ + public Config remove(String key) { + return set(key, null); + } + + + //-------------------------------------------------------------------------------- + // API methods + //-------------------------------------------------------------------------------- + + /** + * Gets the entry with the specified key. + * + *

+ * The key can be in one of the following formats... + *

    + *
  • + * "key" - A value in the default section (i.e. defined above any [section] header). + *
  • + * "section/key" - A value from the specified section. + *
+ * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return The value, or null if the section or key does not exist. + */ + public String getString(String key) { + return getString(key, null); + } + + /** + * Gets the entry with the specified key. + * + *

+ * The key can be in one of the following formats... + *

    + *
  • + * "key" - A value in the default section (i.e. defined above any [section] header). + *
  • + * "section/key" - A value from the specified section. + *
+ * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param def The default value. + * @return The value, or the default value if the section or key does not exist. + */ + public String getString(String key, String def) { + String s = get(key); + if (s == null) + return def; + if (varSession != null) + s = varSession.resolve(s); + return s; + } + + /** + * 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 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 String[] getStringArray(String key, String[] def) { + String s = getString(key); + if (s == null) + return def; + String[] r = isEmpty(s) ? new String[0] : 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 0 if the section or key does not exist or cannot be parsed as an integer. + */ + public int getInt(String key) { + return getInt(key, 0); + } + + /** + * Convenience method for getting int config values. + * + *

+ * "K", "M", and "G" can be used to identify kilo, mega, and giga. + * + *

Example:
+ *
    + *
  • + * "100K" => 1024000 + *
  • + * "100M" => 104857600 + *
+ * + * @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 int getInt(String key, int def) { + String s = getString(key); + if (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 false if the section or key does not exist or cannot be parsed as a boolean. + */ + public 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 boolean getBoolean(String key, boolean def) { + String s = getString(key); + return isEmpty(s) ? def : Boolean.parseBoolean(s); + } + + /** + * Convenience method for getting long config values. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @return The value, or 0 if the section or key does not exist or cannot be parsed as a long. + */ + public long getLong(String key) { + return getLong(key, 0); + } + + /** + * Convenience method for getting long config values. + * + *

+ * "K", "M", and "G" can be used to identify kilo, mega, and giga. + * + *

Example:
+ *
    + *
  • + * "100K" => 1024000 + *
  • + * "100M" => 104857600 + *
+ * + * @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 long getLong(String key, long def) { + String s = getString(key); + if (isEmpty(s)) + return def; + return parseLongWithSuffix(s); + } + + /** + * Gets the entry with the specified key and converts it to the specified value. + * + *

+ * The key can be in one of the following formats... + *

    + *
  • + * "key" - A value in the default section (i.e. defined above any [section] header). + *
  • + * "section/key" - A value from the specified section. + *
+ * + *

+ * The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps). + * + *

Examples:
+ *

+ * ConfigFile cf = ConfigFile.create().build("MyConfig.cfg"); + * + * // Parse into a linked-list of strings. + * List l = cf.getObject("MySection/myListOfStrings", LinkedList.class, String.class); + * + * // Parse into a linked-list of beans. + * List l = cf.getObject("MySection/myListOfBeans", LinkedList.class, MyBean.class); + * + * // Parse into a linked-list of linked-lists of strings. + * List l = cf.getObject("MySection/my2dListOfStrings", LinkedList.class, + * LinkedList.class, String.class); + * + * // Parse into a map of string keys/values. + * Map m = cf.getObject("MySection/myMap", TreeMap.class, String.class, + * String.class); + * + * // Parse into a map containing string keys and values of lists containing beans. + * Map m = cf.getObject("MySection/myMapOfListsOfBeans", TreeMap.class, String.class, + * List.class, MyBean.class); + *

+ * + *

+ * Collection classes are assumed to be followed by zero or one objects indicating the element type. + * + *

+ * Map classes are assumed to be followed by zero or two meta objects indicating the key and value + * types. + * + *

+ * The array can be arbitrarily long to indicate arbitrarily complex data structures. + * + *

Notes:
+ *
    + *
  • + * Use the {@link #getObject(String, Class)} method instead if you don't need a parameterized map/collection. + *
+ * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param type + * The object type to create. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + * @param args + * The type arguments of the class if it's a collection or map. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + *
Ignored if the main type is not a map or collection. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or null if the section or key does not exist. + */ + public T getObject(String key, Type type, Type...args) throws ParseException { + return getObject(key, (Parser)null, type, args); + } + + /** + * Same as {@link #getObject(String, Type, Type...)} but allows you to specify the parser to use to parse the value. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param parser + * The parser to use for parsing the object. + * If null, then uses the predefined parser on the config file. + * @param type + * The object type to create. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + * @param args + * The type arguments of the class if it's a collection or map. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + *
Ignored if the main type is not a map or collection. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or null if the section or key does not exist. + */ + public T getObject(String key, Parser parser, Type type, Type...args) throws ParseException { + assertFieldNotNull(type, "type"); + return parse(getString(key), parser, type, args); + } + + /** + * Same as {@link #getObject(String, Type, Type...)} except optimized for a non-parameterized class. + * + *

+ * This is the preferred parse method for simple types since you don't need to cast the results. + * + *

Examples:
+ *

+ * ConfigFile cf = ConfigFile.create().build("MyConfig.cfg"); + * + * // Parse into a string. + * String s = cf.getObject("MySection/mySimpleString", String.class); + * + * // Parse into a bean. + * MyBean b = cf.getObject("MySection/myBean", MyBean.class); + * + * // Parse into a bean array. + * MyBean[] b = cf.getObject("MySection/myBeanArray", MyBean[].class); + * + * // Parse into a linked-list of objects. + * List l = cf.getObject("MySection/myList", LinkedList.class); + * + * // Parse into a map of object keys/values. + * Map m = cf.getObject("MySection/myMap", TreeMap.class); + *

+ * + * @param The class type of the object being created. + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param type The object type to create. + * @return The parsed object. + * @throws ParseException + * If the input contains a syntax error or is malformed, or is not valid for the specified type. + * @see BeanSession#getClassMeta(Type,Type...) for argument syntax for maps and collections. + */ + public T getObject(String key, Class type) throws ParseException { + return getObject(key, (Parser)null, type); + } + + /** + * Same as {@link #getObject(String, Class)} but allows you to specify the parser to use to parse the value. + * + * @param The class type of the object being created. + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param parser + * The parser to use for parsing the object. + * If null, then uses the predefined parser on the config file. + * @param type The object type to create. + * @return The parsed object. + * @throws ParseException + * If the input contains a syntax error or is malformed, or is not valid for the specified type. + * @see BeanSession#getClassMeta(Type,Type...) for argument syntax for maps and collections. + */ + public T getObject(String key, Parser parser, Class type) throws ParseException { + assertFieldNotNull(type, "c"); + return parse(getString(key), parser, type); + } + + /** + * Gets the entry with the specified key and converts it to the specified value. + * + *

+ * Same as {@link #getObject(String, Class)}, but with a default value. + * + * @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. + * @param type The class to convert the value to. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or null if the section or key does not exist. + */ + public T getObjectWithDefault(String key, T def, Class type) throws ParseException { + return getObjectWithDefault(key, null, def, type); + } + + /** + * Same as {@link #getObjectWithDefault(String, Object, Class)} but allows you to specify the parser to use to parse + * the value. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param parser + * The parser to use for parsing the object. + * If null, then uses the predefined parser on the config file. + * @param def The default value if section or key does not exist. + * @param type The class to convert the value to. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or null if the section or key does not exist. + */ + public T getObjectWithDefault(String key, Parser parser, T def, Class type) throws ParseException { + assertFieldNotNull(type, "c"); + T t = parse(getString(key), parser, type); + return (t == null ? def : t); + } + + /** + * Gets the entry with the specified key and converts it to the specified value. + * + *

+ * Same as {@link #getObject(String, Type, Type...)}, but with a default value. + * + * @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. + * @param type + * The object type to create. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + * @param args + * The type arguments of the class if it's a collection or map. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + *
Ignored if the main type is not a map or collection. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or null if the section or key does not exist. + */ + public T getObjectWithDefault(String key, T def, Type type, Type...args) throws ParseException { + return getObjectWithDefault(key, null, def, type, args); + } + + /** + * Same as {@link #getObjectWithDefault(String, Object, Type, Type...)} but allows you to specify the parser to use + * to parse the value. + * + * @param key The key. See {@link #getString(String)} for a description of the key. + * @param parser + * The parser to use for parsing the object. + * If null, then uses the predefined parser on the config file. + * @param def The default value if section or key does not exist. + * @param type + * The object type to create. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + * @param args + * The type arguments of the class if it's a collection or map. + *
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + *
Ignored if the main type is not a map or collection. + * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file. + * @return The value, or null if the section or key does not exist. + */ + public T getObjectWithDefault(String key, Parser parser, T def, Type type, Type...args) throws ParseException { + assertFieldNotNull(type, "type"); + T t = parse(getString(key), parser, type, args); + return (t == null ? def : t); + } + + /** + * Copies the entries in a section to the specified bean by calling the public setters on that bean. + * + * @param section The section name to write from. + * @param bean The bean to set the properties on. + * @param ignoreUnknownProperties + * If true, don't throw an {@link IllegalArgumentException} if this section contains a key that doesn't + * correspond to a setter method. + * @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 Config writeProperties(String section, Object bean, boolean ignoreUnknownProperties) throws ParseException, IllegalArgumentException, IllegalAccessException, InvocationTargetException { + assertFieldNotNull(bean, "bean"); + + Set keys = configMap.getKeys(section); + if (keys == null) + throw new IllegalArgumentException("Section not found"); + keys = new LinkedHashSet<>(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]; + String propName = Introspector.decapitalize(m.getName().substring(3)); + Object value = getObject(section + '/' + propName, pt); + if (value != null) { + m.invoke(bean, value); + keys.remove(propName); + } + } + } + + if (! (ignoreUnknownProperties || keys.isEmpty())) + throw new ParseException("Invalid properties found in config file section ''{0}'': {1}", section, keys); + + return this; + } + + /** + * Shortcut for calling getSectionAsBean(sectionName, c, false). + * + * @param sectionName The section name to write from. + * @param c The bean class to create. + * @return A new bean instance. + * @throws ParseException + */ + public T getSectionAsBean(String sectionName, Classc) throws ParseException { + return getSectionAsBean(sectionName, c, false); + } + + /** + * Converts this config file section to the specified bean instance. + * + *

+ * Key/value pairs in the config file section get copied as bean property values to the specified bean class. + * + *

Example config file
+ *

+ * [MyAddress] + * name = John Smith + * street = 123 Main Street + * city = Anywhere + * state = NY + * zip = 12345 + *

+ * + *
Example bean
+ *

+ * public class Address { + * public String name, street, city; + * public StateEnum state; + * public int zip; + * } + *

+ * + *
Example usage
+ *

+ * ConfigFile cf = ConfigFile.create().build("MyConfig.cfg"); + * Address myAddress = cf.getSectionAsBean("MySection", Address.class); + *

+ * + * @param section The section name to write from. + * @param c The bean class to create. + * @param ignoreUnknownProperties + * If false, 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 T getSectionAsBean(String section, Class c, boolean ignoreUnknownProperties) throws ParseException { + assertFieldNotNull(c, "c"); + + BeanMap bm = beanSession.newBeanMap(c); + for (String k : configMap.getKeys(section)) { + BeanPropertyMeta bpm = bm.getPropertyMeta(k); + if (bpm == null) { + if (! ignoreUnknownProperties) + throw new ParseException("Unknown property {0} encountered", k); + } else { + bm.put(k, getObject(section + '/' + k, bpm.getClassMeta().getInnerClass())); + } + } + return bm.getBean(); + } + + /** + * Wraps a config file section inside a Java interface so that values in the section can be read and + * write using getters and setters. + * + *
Example config file
+ *

+ * [MySection] + * string = foo + * int = 123 + * enum = ONE + * bean = {foo:'bar',baz:123} + * int3dArray = [[[123,null],null],null] + * bean1d3dListMap = {key:[[[[{foo:'bar',baz:123}]]]]} + *

+ * + *
Example interface
+ *

+ * public interface MyConfigInterface { + * + * String getString(); + * void setString(String x); + * + * int getInt(); + * void setInt(int x); + * + * MyEnum getEnum(); + * void setEnum(MyEnum x); + * + * MyBean getBean(); + * void setBean(MyBean x); + * + * int[][][] getInt3dArray(); + * void setInt3dArray(int[][][] x); + * + * Map<String,List<MyBean[][][]>> getBean1d3dListMap(); + * void setBean1d3dListMap(Map<String,List<MyBean[][][]>> x); + * } + *

+ * + *
Example usage
+ *

+ * ConfigFile cf = ConfigFile.create().build("MyConfig.cfg"); + * + * MyConfigInterface ci = cf.getSectionAsInterface("MySection", MyConfigInterface.class); + * + * int myInt = ci.getInt(); + * + * ci.setBean(new MyBean()); + * + * cf.save(); + *

+ * + * @param sectionName The section name to retrieve as an interface proxy. + * @param c The proxy interface class. + * @return The proxy interface. + */ + @SuppressWarnings("unchecked") + public T getSectionAsInterface(final String sectionName, final Class c) { + assertFieldNotNull(c, "c"); + + if (! c.isInterface()) + throw new UnsupportedOperationException("Class passed to getSectionAsInterface is not an interface."); + + InvocationHandler h = new InvocationHandler() { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + BeanInfo bi = Introspector.getBeanInfo(c, null); + for (PropertyDescriptor pd : bi.getPropertyDescriptors()) { + Method rm = pd.getReadMethod(), wm = pd.getWriteMethod(); + if (method.equals(rm)) + return Config.this.getObject(sectionName + '/' + pd.getName(), rm.getGenericReturnType()); + if (method.equals(wm)) + return Config.this.set(sectionName + '/' + pd.getName(), args[0]); + } + throw new UnsupportedOperationException("Unsupported interface method. method=[ " + method + " ]"); + } + }; + + return (T)Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, h); + } + + /** + * Returns true 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 true if this section contains the specified key and the key has a non-blank value. + */ + public boolean exists(String key) { + return ! isEmpty(getString(key, null)); + } + + /** + * Creates the specified section if it doesn't exist. + * + *

+ * Returns the existing section if it already exists. + * + * @param name + * The section name. + *
Must not be null. + *
Use "default" for the default section. + * @param preLines + * Optional comment and blank lines to add immediately before the section. + *
Can be null. + * @return The appended or existing section. + * @throws UnsupportedOperationException If config file is read only. + */ + public Config setSection(String name, List preLines) { + try { + return setSection(name, preLines, null); + } catch (SerializeException e) { + throw new RuntimeException(e); // Impossible. + } + } + + /** + * Creates the specified section if it doesn't exist. + * + * @param name + * The section name. + *
Must not be null. + *
Use "default" for the default section. + * @param preLines + * Optional comment and blank lines to add immediately before the section. + *
Can be null. + * @param contents + * Values to set in the new section. + *
Can be null. + * @return The appended or existing section. + * @throws SerializeException + * @throws UnsupportedOperationException If config file is read only. + */ + public Config setSection(String name, List preLines, Map contents) throws SerializeException { + configMap.setSection(name, preLines); + + if (contents != null) + for (Map.Entry e : contents.entrySet()) + set(e.getKey(), e.getValue()); + + return this; + } + + /** + * Removes the section with the specified name. + * + * @param name The name of the section to remove + * @return This object (for method chaining). + */ + public Config removeSection(String name) { + configMap.removeSection(name); + return this; + } + + /** + * Saves this config to the store. + * + * @return This object (for method chaining). + * @throws IOException + */ + public Config save() throws IOException { + configMap.save(); + return this; + } + + /** + * Saves this config file to the specified writer as an INI file. + * + *

+ * The writer will automatically be closed. + * + * @param w 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. + */ + @Override /* Writable */ + public Writer writeTo(Writer w) throws IOException { + return configMap.writeTo(w); + } + + /** + * Add a listener to this config to react to modification events. + * + *

+ * Listeners should be removed using {@link #removeListener(ChangeEventListener)}. + * + * @param listener The new listener to add. + * @return This object (for method chaining). + * @throws UnsupportedOperationException If config file is read only. + */ + public Config addListener(ChangeEventListener listener) { + listeners.add(listener); + return this; + } + + /** + * Removes a listener from this config. + * + * @param listener The listener to remove. + * @return This object (for method chaining). + * @throws UnsupportedOperationException If config file is read only. + */ + public Config removeListener(ChangeEventListener listener) { + listeners.remove(listener); + return this; + } + + /** + * Unused. + */ + @Override /* Context */ + public Session createSession(SessionArgs args) { + throw new UnsupportedOperationException(); + } + + /** + * Unused. + */ + @Override /* Context */ + public SessionArgs createDefaultSessionArgs() { + throw new UnsupportedOperationException(); + } + + @Override /* Closeable */ + public void close() throws IOException { + configMap.unregister(this); + closed = true; + } + + @Override /* Object */ + protected void finalize() throws Throwable { + if (! closed) { + System.err.println("Config object not closed."); + } + } + + @Override /* ChangeEventListener */ + public void onChange(List events) { + for (ChangeEventListener l : listeners) + l.onChange(events); + } + + @Override /* Writable */ + public MediaType getMediaType() { + return MediaType.PLAIN; + } + + + //----------------------------------------------------------------------------------------------------------------- + // Private methods + //----------------------------------------------------------------------------------------------------------------- + + private String serialize(Object value, Serializer serializer) throws SerializeException { + if (value == null) + return ""; + if (serializer == null) + serializer = this.serializer; + Class c = value.getClass(); + if (isSimpleType(c)) + return value.toString(); + + if (value instanceof byte[]) { + String s = null; + byte[] b = (byte[])value; + if ("HEX".equals(binaryFormat)) + s = toHex(b); + else if ("SPACED_HEX".equals(binaryFormat)) + s = toSpacedHex(b); + else + s = base64Encode(b); + int l = binaryLineLength; + if (l <= 0 || s.length() <= l) + return s; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i += l) + sb.append('\n').append(s.substring(i, Math.min(s.length(), i + l))); + return sb.toString(); + } + + String r = null; + if (beansOnSeparateLines) + r = "\n" + (String)serializer.serialize(value); + else + r = (String)serializer.serialize(value); + + if (r.startsWith("'")) + return r.substring(1, r.length()-1); + return r; + } + + @SuppressWarnings({ "unchecked" }) + private T parse(String s, Parser parser, Type type, Type...args) throws ParseException { + + if (isEmpty(s)) + return null; + + if (isSimpleType(type)) + return (T)beanSession.convertToType(s, (Class)type); + + if (type == byte[].class) { + if (s.indexOf('\n') != -1) + s = s.replaceAll("\n", ""); + switch (binaryFormat) { + case "HEX": return (T)fromHex(s); + case "SPACED_HEX": return (T)fromSpacedHex(s); + default: return (T)base64Decode(s); + } + } + + char s1 = firstNonWhitespaceChar(s); + if (isArray(type) && s1 != '[') + s = '[' + s + ']'; + else if (s1 != '[' && s1 != '{' && ! "null".equals(s)) + s = '\'' + s + '\''; + + if (parser == null) + parser = this.parser; + + return parser.parse(s, type, args); + } + + private 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()); + } + + private boolean isArray(Type t) { + if (! (t instanceof Class)) + return false; + Class c = (Class)t; + return (c.isArray()); + } + + private String sname(String key) { + assertFieldNotNull(key, "key"); + int i = key.indexOf('/'); + if (i == -1) + return "default"; + return key.substring(0, i); + } + + private String skey(String key) { + int i = key.indexOf('/'); + if (i == -1) + return key; + return key.substring(i+1); + } +} diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigBuilder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigBuilder.java new file mode 100644 index 0000000..0e01141 --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigBuilder.java @@ -0,0 +1,323 @@ +// *************************************************************************************************************************** +// * 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.config.proto; + +import static org.apache.juneau.config.proto.Config.*; + +import java.util.*; + +import org.apache.juneau.*; +import org.apache.juneau.config.encode.*; +import org.apache.juneau.config.store.*; +import org.apache.juneau.json.*; +import org.apache.juneau.parser.*; +import org.apache.juneau.serializer.*; +import org.apache.juneau.svl.*; + +/** + * Builder for creating instances of {@link Config Configs}. + * + *

Example:
+ *

+ * Config cf = Config.create().build("MyConfig.cfg"); + * String setting = cf.get("MySection/mysetting"); + *

+ * + *
See Also:
+ * + */ +public class ConfigBuilder extends ContextBuilder { + + /** + * Constructor, default settings. + */ + public ConfigBuilder() { + super(); + } + + /** + * Constructor. + * + * @param ps The initial configuration settings for this builder. + */ + public ConfigBuilder(PropertyStore ps) { + super(ps); + } + + @Override /* ContextBuilder */ + public Config build() { + return build(Config.class); + } + + + //-------------------------------------------------------------------------------- + // Properties + //-------------------------------------------------------------------------------- + + /** + * Configuration property: Configuration name. + * + *

+ * Specifies the configuration name. + *
This is typically the configuration file name minus the file extension, although + * the name can be anything identifiable by the {@link Store} used for retrieving and storing the configuration. + * + * @param value + * The new value for this property. + *
The default is "Configuration". + * @return This object (for method chaining). + */ + public ConfigBuilder name(String value) { + return set(CONFIG_name, value); + } + + /** + * Configuration property: Configuration store. + * + *

+ * The configuration store used for retrieving and storing configurations. + * + * @param value + * The new value for this property. + *
The default is {@link FileStore#DEFAULT}. + * @return This object (for method chaining). + */ + public ConfigBuilder store(Store value) { + return set(CONFIG_store, value); + } + + /** + * Configuration property: POJO serializer. + * + *

+ * The serializer to use for serializing POJO values. + * + * @param value + * The new value for this property. + *
The default is {@link JsonSerializer#DEFAULT_LAX}. + * @return This object (for method chaining). + */ + public ConfigBuilder serializer(WriterSerializer value) { + return set(CONFIG_serializer, value); + } + + /** + * Configuration property: POJO serializer. + * + *

+ * The serializer to use for serializing POJO values. + * + * @param value + * The new value for this property. + *
The default is {@link JsonSerializer#DEFAULT_LAX}. + * @return This object (for method chaining). + */ + public ConfigBuilder serializer(Class value) { + return set(CONFIG_serializer, value); + } + + /** + * Configuration property: POJO parser. + * + *

+ * The parser to use for parsing values to POJOs. + * + * @param value + * The new value for this property. + *
The default is {@link JsonParser#DEFAULT}. + * @return This object (for method chaining). + */ + public ConfigBuilder parser(ReaderParser value) { + return set(CONFIG_parser, value); + } + + /** + * Configuration property: POJO parser. + * + *

+ * The parser to use for parsing values to POJOs. + * + * @param value + * The new value for this property. + *
The default is {@link JsonParser#DEFAULT}. + * @return This object (for method chaining). + */ + public ConfigBuilder parser(Class value) { + return set(CONFIG_parser, value); + } + + /** + * Configuration property: Value encoder. + * + *

+ * The encoder to use for encoding encoded configuration values. + * + * @param value + * The new value for this property. + *
The default is {@link XorEncoder#INSTANCE}. + * @return This object (for method chaining). + */ + public ConfigBuilder encoder(Encoder value) { + return set(CONFIG_encoder, value); + } + + /** + * Configuration property: Value encoder. + * + *

+ * The encoder to use for encoding encoded configuration values. + * + * @param value + * The new value for this property. + *
The default is {@link XorEncoder#INSTANCE}. + * @return This object (for method chaining). + */ + public ConfigBuilder encoder(Class value) { + return set(CONFIG_encoder, value); + } + + /** + * Configuration property: SVL variable resolver. + * + *

+ * The resolver to use for resolving SVL variables. + * + * @param value + * The new value for this property. + *
The default is {@link VarResolver#DEFAULT}. + * @return This object (for method chaining). + */ + public ConfigBuilder varResolver(VarResolver value) { + return set(CONFIG_varResolver, value); + } + + /** + * Configuration property: SVL variable resolver. + * + *

+ * The resolver to use for resolving SVL variables. + * + * @param value + * The new value for this property. + *
The default is {@link VarResolver#DEFAULT}. + * @return This object (for method chaining). + */ + public ConfigBuilder varResolver(Class value) { + return set(CONFIG_varResolver, value); + } + + /** + * Configuration property: Binary value line length. + * + *

+ * When serializing binary values, lines will be split after this many characters. + *
Use -1 to represent no line splitting. + * + * @param value + * The new value for this property. + *
The default is -1. + * @return This object (for method chaining). + */ + public ConfigBuilder binaryLineLength(int value) { + return set(CONFIG_binaryLineLength, value); + } + + /** + * Configuration property: Binary value format. + * + *

+ * The format to use when persisting byte arrays. + * + *

+ * Possible values: + *

    + *
  • "BASE64" - BASE64-encoded string. + *
  • "HEX" - Hexadecimal. + *
  • "SPACED_HEX" - Hexadecimal with spaces between bytes. + *
+ * + * @param value + * The new value for this property. + *
The default is "BASE64". + * @return This object (for method chaining). + */ + public ConfigBuilder binaryFormat(String value) { + return set(CONFIG_binaryFormat, value); + } + + /** + * Configuration property: Beans on separate lines. + * + *

+ * When enabled, serialized POJOs will be placed on a separate line from the key. + * + * @param value + * The new value for this property. + *
The default is false. + * @return This object (for method chaining). + */ + public ConfigBuilder beansOnSeparateLines(boolean value) { + return set(CONFIG_beansOnSeparateLines, value); + } + + + @Override /* ContextBuilder */ + public ConfigBuilder set(String name, Object value) { + super.set(name, value); + return this; + } + + @Override /* ContextBuilder */ + public ConfigBuilder set(boolean append, String name, Object value) { + super.set(append, name, value); + return this; + } + + @Override /* ContextBuilder */ + public ConfigBuilder set(Map properties) { + super.set(properties); + return this; + } + + @Override /* ContextBuilder */ + public ConfigBuilder add(Map properties) { + super.add(properties); + return this; + } + + @Override /* ContextBuilder */ + public ConfigBuilder addTo(String name, Object value) { + super.addTo(name, value); + return this; + } + + @Override /* ContextBuilder */ + public ConfigBuilder addTo(String name, String key, Object value) { + super.addTo(name, key, value); + return this; + } + + @Override /* ContextBuilder */ + public ConfigBuilder removeFrom(String name, Object value) { + super.removeFrom(name, value); + return this; + } + + @Override /* ContextBuilder */ + public ConfigBuilder apply(PropertyStore copyFrom) { + super.apply(copyFrom); + return this; + } +} diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMod.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMod.java new file mode 100644 index 0000000..ff1a46e --- /dev/null +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMod.java @@ -0,0 +1,103 @@ +// *************************************************************************************************************************** +// * 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.config.proto; + +import java.util.*; + +import org.apache.juneau.internal.*; +import org.apache.juneau.config.encode.*; + +/** + * Identifies the supported modification types for config entries. + */ +public enum ConfigMod { + + /** + * Encoded using the registered {@link Encoder}. + */ + ENCODED("*"); + + private final String c; + + private ConfigMod(String c) { + this.c = c; + } + + /** + * Converts an array of modifiers to a modifier string. + * + * @param mods The modifiers. + * @return A modifier string, or an empty string if there are no modifiers. + */ + public static String asString(ConfigMod...mods) { + if (mods.length == 0) + return ""; + if (mods.length == 1) + return mods[0].c; + StringBuilder sb = new StringBuilder(mods.length); + for (ConfigMod m : mods) + sb.append(m.c); + return sb.toString(); + } + + private static ConfigMod fromChar(char c) { + if (c == '*') + return ENCODED; + return null; + } + + /** + * Converts a modifier string (e.g. "^*") into a list of {@link ConfigMod Modifiers} + * in reverse order of how they appear in the string. + * + * @param s The modifier string. + * @return The list of modifiers, or an empty list if the string is empty or null. + */ + public static List asModifiersReverse(String s) { + if (StringUtils.isEmpty(s)) + return Collections.emptyList(); + if (s.length() == 1) { + ConfigMod m = fromChar(s.charAt(0)); + return m == null ? Collections.emptyList() : Collections.singletonList(m); + } + List l = new ArrayList<>(s.length()); + for (int i = s.length()-1; i >= 0; i--) { + ConfigMod m = fromChar(s.charAt(i)); + if (m != null) + l.add(m); + } + return l; + } + + /** + * Converts a modifier string (e.g. "^*") into a list of {@link ConfigMod Modifiers}. + * + * @param s The modifier string. + * @return The list of modifiers, or an empty list if the string is empty or null. + */ + public static List asModifiers(String s) { + if (StringUtils.isEmpty(s)) + return Collections.emptyList(); + if (s.length() == 1) { + ConfigMod m = fromChar(s.charAt(0)); + return m == null ? Collections.emptyList() : Collections.singletonList(m); + } + List l = new ArrayList<>(s.length()); + for (int i = 0; i < s.length(); i++) { + ConfigMod m = fromChar(s.charAt(i)); + if (m != null) + l.add(m); + } + return l; + } +} diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java index 2c4dbe8..7264d64 100644 --- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java @@ -109,34 +109,43 @@ public class ConfigEntry { public boolean hasModifier(char m) { return modifiers.indexOf(m) != -1; } + + /** + * Returns the modifiers for this entry. + * + * @return The modifiers for this entry, or an empty string if it has no modifiers. + */ + public String getModifiers() { + return modifiers; + } - Writer writeTo(Writer out) throws IOException { + Writer writeTo(Writer w) throws IOException { if (value == null) - return out; + return w; for (String pl : preLines) - out.append(pl).append('\n'); + w.append(pl).append('\n'); if (rawLine != null) { String l = rawLine; if (l.indexOf('\n') != -1) l = l.replaceAll("(\\r?\\n)", "$1\t"); - out.append(l).append('\n'); + w.append(l).append('\n'); } else { - out.append(key); - out.append(modifiers); - out.append(" = "); + w.append(key); + w.append(modifiers); + w.append(" = "); String val = value; if (val.indexOf('\n') != -1) val = val.replaceAll("(\\r?\\n)", "$1\t"); if (val.indexOf('#') != -1) val = val.replaceAll("#", "\\\\#"); - out.append(val); + w.append(val); if (comment != null) - out.append(" # ").append(comment); + w.append(" # ").append(comment); - out.append('\n'); + w.append('\n'); } - return out; + return w; } } \ No newline at end of file diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java index f83388e..4756127 100644 --- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java +++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java @@ -21,12 +21,13 @@ import java.util.concurrent.locks.*; import org.apache.juneau.*; import org.apache.juneau.config.event.*; +import org.apache.juneau.http.*; import org.apache.juneau.internal.*; /** * Represents the parsed contents of a configuration. */ -public class ConfigMap implements StoreListener { +public class ConfigMap implements StoreListener, Writable { private final Store store; // The store that created this object. private volatile String contents; // The original contents of this object. @@ -210,6 +211,19 @@ public class ConfigMap implements StoreListener { } } + /** + * Returns the keys of the entries in the specified section. + * + * @param section + * The section name. + *
Must not be null. + * @return + * An unmodifiable set of keys, or null if the section doesn't exist. + */ + public Set getKeys(String section) { + ConfigSection cs = entries.get(section); + return cs == null ? null : Collections.unmodifiableSet(cs.entries.keySet()); + } //----------------------------------------------------------------------------------------------------------------- // Setters @@ -242,7 +256,7 @@ public class ConfigMap implements StoreListener { public ConfigMap removeSection(String section) { return applyChange(true, ChangeEvent.removeSection(section)); } - + /** * Sets the pre-lines on an entry without modifying any other attributes. * @@ -433,7 +447,7 @@ public class ConfigMap implements StoreListener { * @param listener The new listener. * @return This object (for method chaining). */ - public ConfigMap registerListener(ChangeEventListener listener) { + public ConfigMap register(ChangeEventListener listener) { listeners.add(listener); return this; } @@ -444,7 +458,7 @@ public class ConfigMap implements StoreListener { * @param listener The listener to remove. * @return This object (for method chaining). */ - public ConfigMap unregisterListener(ChangeEventListener listener) { + public ConfigMap unregister(ChangeEventListener listener) { listeners.remove(listener); return this; } @@ -469,9 +483,86 @@ public class ConfigMap implements StoreListener { signal(changes); } + @Override /* Object */ + public String toString() { + readLock(); + try { + return asString(); + } finally { + readUnlock(); + } + } + + @Override /* Writable */ + public Writer writeTo(Writer w) throws IOException { + for (ConfigSection cs : entries.values()) + cs.writeTo(w); + return w; + } + + @Override /* Writable */ + public MediaType getMediaType() { + return MediaType.PLAIN; + } + + + //-------------------------------------------------------------------------------- + // Private methods + //-------------------------------------------------------------------------------- + + private void readLock() { + lock.readLock().lock(); + } + + private void readUnlock() { + lock.readLock().unlock(); + } + + private void writeLock() { + lock.writeLock().lock(); + } + + private void writeUnlock() { + lock.writeLock().unlock(); + } + + private boolean isValidSectionName(String s) { + return "default".equals(s) || isValidNewSectionName(s); + } + + private boolean isValidKeyName(String s) { + if (s == null) + return false; + s = s.trim(); + if (s.isEmpty()) + return false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#') + return false; + } + return true; + } + + private boolean isValidNewSectionName(String s) { + if (s == null) + return false; + s = s.trim(); + if (s.isEmpty()) + return false; + if ("default".equals(s)) + return false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '/' || c == '\\' || c == '[' || c == ']') + return false; + } + return true; + } + private void signal(List changes) { for (ChangeEventListener l : listeners) - l.onEvents(changes); + l.onChange(changes); } private List findDiffs(String updatedContents) { @@ -510,6 +601,18 @@ public class ConfigMap implements StoreListener { return changes; } + // This method should only be called from behind a lock. + private String asString() { + try { + StringWriter sw = new StringWriter(); + for (ConfigSection cs : entries.values()) + cs.writeTo(sw); + return sw.toString(); + } catch (IOException e) { + throw new RuntimeException(e); // Not possible. + } + } + //--------------------------------------------------------------------------------------------- // ConfigSection @@ -586,97 +689,22 @@ public class ConfigMap implements StoreListener { return this; } - Writer writeTo(Writer out) throws IOException { + Writer writeTo(Writer w) throws IOException { for (String s : preLines) - out.append(s).append('\n'); + w.append(s).append('\n'); if (! name.equals("default")) - out.append(rawLine).append('\n'); + w.append(rawLine).append('\n'); else { // Need separation between default prelines and first-entry prelines. if (! preLines.isEmpty()) - out.append('\n'); + w.append('\n'); } for (ConfigEntry e : entries.values()) - e.writeTo(out); + e.writeTo(w); - return out; - } - } - - @Override /* Object */ - public String toString() { - readLock(); - try { - return asString(); - } finally { - readUnlock(); + return w; } } - - String asString() { - try { - StringWriter sw = new StringWriter(); - for (ConfigSection cs : entries.values()) - cs.writeTo(sw); - return sw.toString(); - } catch (IOException e) { - throw new RuntimeException(e); // Not possible. - } - } - - private boolean isValidNewSectionName(String s) { - if (s == null) - return false; - s = s.trim(); - if (s.isEmpty()) - return false; - if ("default".equals(s)) - return false; - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '/' || c == '\\' || c == '[' || c == ']') - return false; - } - return true; - } - - private boolean isValidSectionName(String s) { - return "default".equals(s) || isValidNewSectionName(s); - } - - private boolean isValidKeyName(String s) { - if (s == null) - return false; - s = s.trim(); - if (s.isEmpty()) - return false; - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#') - return false; - } - return true; - } - - //-------------------------------------------------------------------------------- - // Private methods - //-------------------------------------------------------------------------------- - - void readLock() { - lock.readLock().lock(); - } - - void readUnlock() { - lock.readLock().unlock(); - } - - void writeLock() { - lock.writeLock().lock(); - } - - void writeUnlock() { - lock.writeLock().unlock(); - } } diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java index bec142e..5e575d3 100644 --- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java +++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java @@ -45,12 +45,12 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("default", "foo", "baz"); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("foo = baz|", cm.toString()); } @@ -72,12 +72,12 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("S1", "foo", "baz"); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("[S1]|foo = baz|", cm.toString()); } @@ -101,13 +101,13 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("default", "k", "vb"); cm.setValue("S1", "k1", "v1b"); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("k = vb|[S1]|k1 = v1b|", cm.toString()); } @@ -127,13 +127,13 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setEntry("default", "k", "kb", "^*", "C", Arrays.asList("#k")); cm.setEntry("S1", "k1", "k1b", "^*", "C1", Arrays.asList("#k1")); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("#k|k^* = kb # C|[S1]|#k1|k1^* = k1b # C1|", cm.toString()); } @@ -159,13 +159,13 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setEntry("default", "k", "kb", "^*", "Cb", Arrays.asList("#kb")); cm.setEntry("S1", "k1", "k1b", "^*", "Cb1", Arrays.asList("#k1b")); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("#kb|k^* = kb # Cb|#S1|[S1]|#k1b|k1^* = k1b # Cb1|", cm.toString()); } @@ -192,13 +192,13 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("default", "k", null); cm.setValue("S1", "k1", null); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("[S1]|", cm.toString()); } @@ -224,13 +224,13 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("default", "k", null); cm.setValue("S1", "k1", null); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("#S1|[S1]|", cm.toString()); } @@ -254,7 +254,7 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setSection("default", Arrays.asList("#D1")); cm.setSection("S1", Arrays.asList("#S1")); cm.setSection("S2", null); @@ -263,7 +263,7 @@ public class ConfigMapListenerTest { cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("#D1||#S1|[S1]|[S2]|[S3]|k3 = v3|", cm.toString()); } @@ -289,7 +289,7 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setSection("default", Arrays.asList("#Db")); cm.setSection("S1", Arrays.asList("#S1b")); cm.setSection("S2", null); @@ -298,7 +298,7 @@ public class ConfigMapListenerTest { cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("#Db||#S1b|[S1]|[S2]|[S3]|k3 = v3|", cm.toString()); } @@ -334,7 +334,7 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.removeSection("default"); cm.removeSection("S1"); cm.removeSection("S2"); @@ -342,7 +342,7 @@ public class ConfigMapListenerTest { cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("", cm.toString()); } @@ -365,7 +365,7 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); s.update("Foo", "#Da", "", @@ -382,7 +382,7 @@ public class ConfigMapListenerTest { ); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("#Da||k = v # cv||#S1|[S1]|#k1|k1 = v1 # cv1|[S2]|#k2|k2 = v2 # cv2|[S3]|", cm.toString()); } @@ -411,7 +411,7 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("S2", "k2", "v2b"); s.update("Foo", "[S1]", @@ -420,7 +420,7 @@ public class ConfigMapListenerTest { cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("[S1]|k1 = v1b|[S2]|k2 = v2b|", cm.toString()); } @@ -449,7 +449,7 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("S1", "k1", "v1c"); s.update("Foo", "[S1]", @@ -458,7 +458,7 @@ public class ConfigMapListenerTest { cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("[S1]|k1 = v1c|", cm.toString()); } @@ -495,12 +495,12 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("S1", "k1", "v1c"); cm.save(); wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("[S1]|k1 = v1c|", cm.toString()); @@ -540,7 +540,7 @@ public class ConfigMapListenerTest { }; ConfigMap cm = s.getMap("Foo"); - cm.registerListener(l); + cm.register(l); cm.setValue("S1", "k1", "v1c"); try { cm.save(); @@ -550,7 +550,7 @@ public class ConfigMapListenerTest { } wait(latch); assertNull(l.error); - cm.unregisterListener(l); + cm.unregister(l); assertTextEquals("[S1]|k1 = v1c|", cm.toString()); @@ -575,7 +575,7 @@ public class ConfigMapListenerTest { } @Override - public void onEvents(List events) { + public void onChange(List events) { try { check(events); } catch (Exception e) { diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java index bf507c8..fe926c0 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java @@ -28,9 +28,10 @@ public interface Writable { * Serialize this object to the specified writer. * * @param w The writer to write to. + * @return The same writer passed in. * @throws IOException */ - void writeTo(Writer w) throws IOException; + Writer writeTo(Writer w) throws IOException; /** * Returns the serialized media type for this resource (e.g. "text/html") diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java index b71a6a7..624d9b3 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java @@ -1579,6 +1579,8 @@ public final class StringUtils { public static List splitEqually(String s, int size) { if (s == null) return null; + if (size <= 0) + return Collections.singletonList(s); List l = new ArrayList<>((s.length() + size - 1) / size); diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java index 17ed8fa..58ea67d 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java @@ -45,9 +45,9 @@ public class StringMessage implements CharSequence, Writable { } @Override /* Writable */ - public void writeTo(Writer w) throws IOException { + public Writer writeTo(Writer w) throws IOException { w.write(toString()); - + return w; } @Override /* Writable */ diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java index 2982db6..18546ab 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java @@ -83,9 +83,10 @@ public class StringObject implements CharSequence, Writable { } @Override /* Writable */ - public void writeTo(Writer w) throws IOException { + public Writer writeTo(Writer w) throws IOException { try { s.serialize(o, w); + return w; } catch (SerializeException e) { throw new IOException(e); } diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java index 22275ad..c235cbd 100644 --- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java +++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java @@ -114,13 +114,14 @@ public class ReaderResource implements Writable { } @Override /* Writeable */ - public void writeTo(Writer w) throws IOException { + public Writer writeTo(Writer w) throws IOException { for (String s : contents) { if (varSession != null) varSession.resolveTo(s, w); else w.write(s); } + return w; } @Override /* Writeable */ -- To stop receiving notification emails like this one, please contact jamesbognar@apache.org.