Repository: incubator-juneau Updated Branches: refs/heads/master c68cc34d5 -> ca59d8a4e
http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/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 index d84242e..da2e09f 100644 --- a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFile.java +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFile.java @@ -57,9 +57,24 @@ public abstract class ConfigFile implements Map<String,Section> { * @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 SerializeException If the object value could not be converted to a JSON string for some reason. * @throws UnsupportedOperationException If config file is read only. */ - public abstract String put(String sectionName, String sectionKey, Object value, boolean encoded); + public abstract String put(String sectionName, String sectionKey, Object value, boolean encoded) throws SerializeException; + + /** + * Identical to {@link #put(String, String, Object, boolean)} except used when the value is a simple string + * to avoid having to catch a {@link SerializeException}. + * + * @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, String value, boolean encoded); + /** * Removes an antry from this config file. @@ -158,20 +173,38 @@ public abstract class ConfigFile implements Map<String,Section> { public abstract ConfigFile clearHeaderComments(String section); /** - * Returns the serializer in use for this config file. + * Returns the reusable bean session associated with this config file. + * <p> + * Used for performing simple datatype conversions. * - * @return This object (for method chaining). - * @throws SerializeException If no serializer is defined on this config file. + * @return The reusable bean session associated with this config file. */ - protected abstract WriterSerializer getSerializer() throws SerializeException; + protected abstract BeanSession getBeanSession(); /** - * Returns the parser in use for this config file. + * Converts the specified object to a string. + * <p> + * The serialized output is identical to LAX JSON (JSON with unquoted attributes) except for the following exceptions: + * <ul> + * <li>Top level strings are not quoted. + * </ul> * - * @return This object (for method chaining). - * @throws ParseException If no parser is defined on this config file. + * @param o The object to serialize. + * @return The serialized object. + * @throws SerializeException */ - protected abstract ReaderParser getParser() throws ParseException; + protected abstract String serialize(Object o) throws SerializeException; + + /** + * Converts the specified string to an object of the specified type. + * + * @param s The string to parse. + * @param type The data type to create. + * @param args The generic type arguments if the type is a {@link Collection} or {@link Map} + * @return The parsed object. + * @throws ParseException + */ + protected abstract <T> T parse(String s, Type type, Type...args) throws ParseException; /** * Places a read lock on this config file. @@ -222,81 +255,173 @@ public abstract class ConfigFile implements Map<String,Section> { * <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. + * The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps). + * + * <h5 class='section'>Examples:</h5> + * <p class='bcode'> + * ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); + * + * <jc>// Parse into a linked-list of strings.</jc> + * List l = cf.getObject(<js>"MySection/myListOfStrings"</js>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); + * + * <jc>// Parse into a linked-list of beans.</jc> + * List l = cf.getObject(<js>"MySection/myListOfBeans"</js>, LinkedList.<jk>class</jk>, MyBean.<jk>class</jk>); + * + * <jc>// Parse into a linked-list of linked-lists of strings.</jc> + * List l = cf.getObject(<js>"MySection/my2dListOfStrings"</js>, LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); + * + * <jc>// Parse into a map of string keys/values.</jc> + * Map m = cf.getObject(<js>"MySection/myMap"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); + * + * <jc>// Parse into a map containing string keys and values of lists containing beans.</jc> + * Map m = cf.getObject(<js>"MySection/myMapOfListsOfBeans"</js>, TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>); + * </p> + * <p> + * <code>Collection</code> classes are assumed to be followed by zero or one objects indicating the element type. + * <p> + * <code>Map</code> classes are assumed to be followed by zero or two meta objects indicating the key and value types. * <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>. + * The array can be arbitrarily long to indicate arbitrarily complex data structures. + * <p> + * <h5 class='section'>Notes:</h5> + * <ul> + * <li>Use the {@link #getObject(String, Class)} method instead if you don't need a parameterized map/collection. + * </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 type The object type to create. + * <br>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. + * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + * <br>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 <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); + public final <T> T getObject(String key, Type type, Type...args) throws ParseException { + assertFieldNotNull(key, "key"); + assertFieldNotNull(type, "type"); + return parse(getString(key), type, args); } /** - * Gets the entry with the specified key and converts it to the specified value.. + * Same as {@link #getObject(String, Type, Type...)} except optimized for a non-parameterized class. * <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> + * This is the preferred parse method for simple types since you don't need to cast the results. + * <h5 class='section'>Examples:</h5> + * <p class='bcode'> + * ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); + * + * <jc>// Parse into a string.</jc> + * String s = cf.getObject(<js>"MySection/mySimpleString"</js>, String.<jk>class</jk>); + * + * <jc>// Parse into a bean.</jc> + * MyBean b = cf.getObject(<js>"MySection/myBean"</js>, MyBean.<jk>class</jk>); + * + * <jc>// Parse into a bean array.</jc> + * MyBean[] ba = cf.getObject(<js>"MySection/myBeanArray"</js>, MyBean[].<jk>class</jk>); + * + * <jc>// Parse into a linked-list of objects.</jc> + * List l = cf.getObject(<js>"MySection/myList"</js>, LinkedList.<jk>class</jk>); + * + * <jc>// Parse into a map of object keys/values.</jc> + * Map m = cf.getObject(<js>"MySection/myMap"</js>, TreeMap.<jk>class</jk>); + * </p> + * + * @param <T> 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 final <T> T getObject(String key, Class<T> type) throws ParseException { + assertFieldNotNull(key, "key"); + assertFieldNotNull(type, "c"); + return parse(getString(key), type); + } + + + /** + * Gets the entry with the specified key and converts it to the specified value. + * <p> + * Same as {@link #getObject(String, Class)}, but with a default value. * - * @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. + * @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 <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"); + public final <T> T getObjectWithDefault(String key, T def, Class<T> type) throws ParseException { assertFieldNotNull(key, "key"); - return getObject(c, getSectionName(key), getSectionKey(key), def); + assertFieldNotNull(type, "c"); + T t = parse(getString(key), type); + return (t == null ? def : t); } /** - * Same as {@link #getObject(Class, String, Object)}, but value is referenced through section name and key instead of full key. + * Gets the entry with the specified key and converts it to the specified value. + * <p> + * 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. + * <br>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. + * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + * <br>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 <jk>null</jk> if the section or key does not exist. + */ + public final <T> T getObjectWithDefault(String key, T def, Type type, Type...args) throws ParseException { + assertFieldNotNull(key, "key"); + assertFieldNotNull(type, "type"); + T t = parse(getString(key), type, args); + return (t == null ? def : t); + } + + /** + * Gets the entry with the specified key and converts it to the specified value. + * <p> + * Same as {@link #getObject(String, Class)}, but used when key is already broken into section/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. + * @param c 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 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); + public final <T> T getObject(String sectionName, String sectionKey, Class<T> c) throws ParseException { + assertFieldNotNull(sectionName, "sectionName"); + assertFieldNotNull(sectionKey, "sectionKey"); + return parse(get(sectionName, sectionKey), c); + } + + /** + * Gets the entry with the specified key and converts it to the specified value. + * <p> + * Same as {@link #getObject(String, Type, Type...)}, but used when key is already broken into section/key. + * + * @param sectionName The section name. Must not be <jk>null</jk>. + * @param sectionKey The section key. Must not be <jk>null</jk>. + * @param type The object type to create. + * <br>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. + * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} + * <br>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 <jk>null</jk> if the section or key does not exist. + */ + public final <T> T getObject(String sectionName, String sectionKey, Type type, Type...args) throws ParseException { + assertFieldNotNull(sectionName, "sectionName"); + assertFieldNotNull(sectionKey, "sectionKey"); + return parse(get(sectionName, sectionKey), type, args); } /** @@ -430,33 +555,7 @@ public abstract class ConfigFile implements Map<String,Section> { */ 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 static boolean isSimpleType(Class<?> c) { - return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class); + return put(getSectionName(key), getSectionKey(key), serialize(value), encoded); } /** @@ -510,7 +609,7 @@ public abstract class ConfigFile implements Map<String,Section> { 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); + Object value = getObject(sectionName, propName, pt); if (value != null) { m.invoke(bean, value); om.put(propName, value); @@ -528,7 +627,7 @@ public abstract class ConfigFile implements Map<String,Section> { } /** - * Shortcut for calling <code>asBean(sectionName, c, <jk>false</jk>)</code>. + * Shortcut for calling <code>getSectionAsBean(sectionName, c, <jk>false</jk>)</code>. * * @param sectionName The section name to write from. * @param c The bean class to create. @@ -541,6 +640,33 @@ public abstract class ConfigFile implements Map<String,Section> { /** * Converts this config file section to the specified bean instance. + * <p> + * Key/value pairs in the config file section get copied as bean property values to the specified bean class. + * <p> + * <h6 class='figure'>Example config file</h6> + * <p class='bcode'> + * <cs>[MyAddress]</cs> + * <ck>name</ck> = <cv>John Smith</cv> + * <ck>street</ck> = <cv>123 Main Street</cv> + * <ck>city</ck> = <cv>Anywhere</cv> + * <ck>state</ck> = <cv>NY</cv> + * <ck>zip</ck> = <cv>12345</cv> + * </p> + * + * <h6 class='figure'>Example bean</h6> + * <p class='bcode'> + * <jk>public class</jk> Address { + * public String name, street, city; + * public StateEnum state; + * public int zip; + * } + * </p> + * + * <h6 class='figure'>Example usage</h6> + * <p class='bcode'> + * ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); + * Address myAddress = cf.getSectionAsBean(<js>"MySection"</js>, Address.<jk>class</jk>); + * </p> * * @param sectionName The section name to write from. * @param c The bean class to create. @@ -553,14 +679,14 @@ public abstract class ConfigFile implements Map<String,Section> { assertFieldNotNull(c, "c"); readLock(); try { - BeanMap<T> bm = getParser().getBeanContext().createSession().newBeanMap(c); + BeanMap<T> bm = getBeanSession().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)); + bm.put(k, getObject(sectionName + '/' + k, bpm.getClassMeta().getInnerClass())); } } return bm.getBean(); @@ -570,6 +696,88 @@ public abstract class ConfigFile implements Map<String,Section> { } /** + * Wraps a config file section inside a Java interface so that values in the section can be read and + * write using getters and setters. + * <p> + * <h6 class='figure'>Example config file</h6> + * <p class='bcode'> + * <cs>[MySection]</cs> + * <ck>string</ck> = <cv>foo</cv> + * <ck>int</ck> = <cv>123</cv> + * <ck>enum</ck> = <cv>ONE</cv> + * <ck>bean</ck> = <cv>{foo:'bar',baz:123}</cv> + * <ck>int3dArray</ck> = <cv>[[[123,null],null],null]</cv> + * <ck>bean1d3dListMap</ck> = <cv>{key:[[[[{foo:'bar',baz:123}]]]]}</cv> + * </p> + * + * <h6 class='figure'>Example interface</h6> + * <p class='bcode'> + * <jk>public interface</jk> MyConfigInterface { + * + * String getString(); + * <jk>void</jk> setString(String x); + * + * <jk>int</jk> getInt(); + * <jk>void</jk> setInt(<jk>int</jk> x); + * + * MyEnum getEnum(); + * <jk>void</jk> setEnum(MyEnum x); + * + * MyBean getBean(); + * <jk>void</jk> setBean(MyBean x); + * + * <jk>int</jk>[][][] getInt3dArray(); + * <jk>void</jk> setInt3dArray(<jk>int</jk>[][][] x); + * + * Map<String,List<MyBean[][][]>> getBean1d3dListMap(); + * <jk>void</jk> setBean1d3dListMap(Map<String,List<MyBean[][][]>> x); + * } + * </p> + * + * <h6 class='figure'>Example usage</h6> + * <p class='bcode'> + * ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); + * + * MyConfigInterface ci = cf.getSectionAsInterface(<js>"MySection"</js>, MyConfigInterface.<jk>class</jk>); + * + * <jk>int</jk> myInt = ci.getInt(); + * + * ci.setBean(<jk>new</jk> MyBean()); + * + * cf.save(); + * </p> + * + * @param sectionName The section name to retrieve as an interface proxy. + * @param c The proxy interface class. + * @return The proxy interface. + */ + @SuppressWarnings("unchecked") + public final <T> T getSectionAsInterface(final String sectionName, final Class<T> 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 ConfigFile.this.getObject(sectionName, pd.getName(), rm.getGenericReturnType()); + if (method.equals(wm)) + return ConfigFile.this.put(sectionName, pd.getName(), args[0], false); + } + throw new UnsupportedOperationException("Unsupported interface method. method=[ " + method + " ]"); + } + }; + + return (T)Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, h); + } + + /** * 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. http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileBuilder.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileBuilder.java b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileBuilder.java index a699b4f..5d44f10 100644 --- a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileBuilder.java +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileBuilder.java @@ -69,7 +69,7 @@ public class ConfigFileBuilder { } /** - * Specify the parser to use for parsing POJOs when using {@link ConfigFile#getObject(Class,String)}. + * Specify the parser to use for parsing POJOs when using {@link ConfigFile#getObject(String,Class)}. * <p> * The default value for this setting is {@link JsonParser#DEFAULT} * http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/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 index f1ef0eb..dc784b5 100644 --- a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java @@ -16,6 +16,7 @@ import static org.apache.juneau.ini.ConfigUtils.*; import static org.apache.juneau.internal.ThrowableUtils.*; import java.io.*; +import java.lang.reflect.*; import java.nio.charset.*; import java.util.*; import java.util.concurrent.locks.*; @@ -37,6 +38,7 @@ public final class ConfigFileImpl extends ConfigFile { private final Encoder encoder; private final WriterSerializer serializer; private final ReaderParser parser; + private final BeanSession pBeanSession; private final Charset charset; final List<ConfigFileListener> listeners = Collections.synchronizedList(new ArrayList<ConfigFileListener>()); @@ -66,7 +68,7 @@ public final class ConfigFileImpl extends ConfigFile { * 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. + * @param parser The parser to use for parsing POJOs in the {@link #getObject(String,Class)} method. * If <jk>null</jk>, defaults to {@link JsonParser#DEFAULT}. * @param charset The charset on the files. * If <jk>null</jk>, defaults to {@link Charset#defaultCharset()}. @@ -85,6 +87,7 @@ public final class ConfigFileImpl extends ConfigFile { for (Section s : sections.values()) s.setReadOnly(); } + this.pBeanSession = this.parser.getBeanContext().createSession(); } /** @@ -193,6 +196,43 @@ public final class ConfigFileImpl extends ConfigFile { return this; } + @Override /* ConfigFile */ + protected String serialize(Object value) throws SerializeException { + if (value == null) + return ""; + Class<?> c = value.getClass(); + if (isSimpleType(c)) + return value.toString(); + String s = serializer.toString(value); + if (s.startsWith("'")) + return s.substring(1, s.length()-1); + return s; + } + + @Override /* ConfigFile */ + @SuppressWarnings({ "unchecked" }) + protected <T> T parse(String s, Type type, Type...args) throws ParseException { + + if (StringUtils.isEmpty(s)) + return null; + + if (isSimpleType(type)) + return (T)pBeanSession.convertToType(s, (Class<?>)type); + + char s1 = StringUtils.charAt(s, 0); + if (s1 != '[' && s1 != '{' && ! "null".equals(s)) + s = '\'' + s + '\''; + + return parser.parse(s, type, args); + } + + private static boolean isSimpleType(Type t) { + if (! (t instanceof Class)) + return false; + Class<?> c = (Class<?>)t; + return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum()); + } + //-------------------------------------------------------------------------------- // Map methods @@ -429,10 +469,17 @@ public final class ConfigFileImpl extends ConfigFile { } @Override /* ConfigFile */ - public String put(String sectionName, String sectionKey, Object value, boolean encoded) { + public String put(String sectionName, String sectionKey, Object value, boolean encoded) throws SerializeException { assertFieldNotNull(sectionKey, "sectionKey"); Section s = getSection(sectionName, true); - return s.put(sectionKey, value.toString(), encoded); + return s.put(sectionKey, serialize(value), encoded); + } + + @Override /* ConfigFile */ + public String put(String sectionName, String sectionKey, String value, boolean encoded) { + assertFieldNotNull(sectionKey, "sectionKey"); + Section s = getSection(sectionName, true); + return s.put(sectionKey, value, encoded); } @Override /* ConfigFile */ @@ -657,17 +704,8 @@ public final class ConfigFileImpl extends ConfigFile { } @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; + protected BeanSession getBeanSession() { + return pBeanSession; } @Override /* ConfigFile */ http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/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 index 62f3899..5407c0f 100644 --- a/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java +++ b/juneau-core/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java @@ -15,6 +15,7 @@ package org.apache.juneau.ini; import static org.apache.juneau.internal.ThrowableUtils.*; import java.io.*; +import java.lang.reflect.*; import java.util.*; import org.apache.juneau.*; @@ -233,13 +234,8 @@ public final class ConfigFileWrapped extends ConfigFile { } @Override /* ConfigFile */ - protected WriterSerializer getSerializer() throws SerializeException { - return cf.getSerializer(); - } - - @Override /* ConfigFile */ - protected ReaderParser getParser() throws ParseException { - return cf.getParser(); + protected BeanSession getBeanSession() { + return cf.getBeanSession(); } @Override /* ConfigFile */ @@ -251,7 +247,12 @@ public final class ConfigFileWrapped extends ConfigFile { } @Override /* ConfigFile */ - public String put(String sectionName, String sectionKey, Object value, boolean encoded) { + public String put(String sectionName, String sectionKey, String value, boolean encoded) { + return cf.put(sectionName, sectionKey, value, encoded); + } + + @Override /* ConfigFile */ + public String put(String sectionName, String sectionKey, Object value, boolean encoded) throws SerializeException { return cf.put(sectionName, sectionKey, value, encoded); } @@ -274,4 +275,14 @@ public final class ConfigFileWrapped extends ConfigFile { protected void readUnlock() { cf.readUnlock(); } + + @Override /* ConfigFile */ + protected String serialize(Object o) throws SerializeException { + return cf.serialize(o); + } + + @Override /* ConfigFile */ + protected <T> T parse(String s, Type type, Type... args) throws ParseException { + return cf.parse(s, type, args); + } } http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java b/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java index 92ab945..490eed1 100644 --- a/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java +++ b/juneau-core/src/main/java/org/apache/juneau/json/JsonParser.java @@ -590,7 +590,7 @@ public class JsonParser extends ReaderParser { break; } } else { - if (c == ',' || c == '}' || session.isWhitespace(c)) { + if (c == ',' || c == '}' || c == ']' || session.isWhitespace(c)) { s = r.getMarked(0, -1); r.unread(); break; http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-core/src/main/javadoc/overview.html ---------------------------------------------------------------------- diff --git a/juneau-core/src/main/javadoc/overview.html b/juneau-core/src/main/javadoc/overview.html index 62ed843..980b84a 100644 --- a/juneau-core/src/main/javadoc/overview.html +++ b/juneau-core/src/main/javadoc/overview.html @@ -1373,14 +1373,14 @@ <cc># Default section</cc> <ck>key1</ck> = <cv>1</cv> <ck>key2</ck> = <cv>true</cv> - <ck>key3</ck> = <cv>1,2,3</cv> + <ck>key3</ck> = <cv>[1,2,3]</cv> <ck>key4</ck> = <cv>http://foo</cv> <cc># Section 1</cc> <cs>[Section1]</cs> <ck>key1</ck> = <cv>2</cv> <ck>key2</ck> = <cv>false</cv> - <ck>key3</ck> = <cv>4,5,6</cv> + <ck>key3</ck> = <cv>[4,5,6]</cv> <ck>key4</ck> = <cv>http://bar</cv> </p> <p> @@ -1393,7 +1393,7 @@ URL key4; <jc>// Load our config file</jc> - ConfigFile f = <jk>new</jk> ConfigFileBuilder().build(<js>"MyIniFile.cfg"</js>); + ConfigFile f = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); <jc>// Read values from default section</jc> key1 = f.getInt(<js>"key1"</js>); @@ -1412,12 +1412,12 @@ </p> <p class='bcode'> <jc>// Construct the sample INI file programmatically</jc> - ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyIniFile.cfg"</js>) + ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>) .addLines(<jk>null</jk>, <js>"# Default section"</js>, <js>"key1 = 1"</js>, <js>"key2 = true"</js>, - <js>"key3 = 1,2,3"</js>, + <js>"key3 = [1,2,3]"</js>, <js>"key4 = http://foo"</js>, <js>""</js>) .addHeaderComments(<js>"Section1"</js>, @@ -1425,7 +1425,7 @@ .addLines(<js>"Section1"</js>, <js>"key1 = 2"</js>, <js>"key2 = false"</js>, - <js>"key3 = 4,5,6"</js>, + <js>"key3 = [4,5,6]"</js>, <js>"key4 = http://bar"</js>) .save(); </p> @@ -1434,7 +1434,7 @@ </p> <p class='bcode'> <jc>// Construct the sample INI file programmatically</jc> - ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyIniFile.cfg"</js>) + ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>) .addLines(<jk>null</jk>, <js>"# Default section"</js>) .addHeaderComments(<js>"section1"</js>, @@ -1450,11 +1450,14 @@ cf.save(); </p> <p> + Values are LAX JSON (i.e. unquoted attributes, single quotes) except for top-level strings which are left unquoted. + Any parsable object types are supported as values (e.g. arrays, collections, beans, swappable objects, enums, etc...). + </p> + <p> The config file looks deceptively simple, the config file API is a very powerful feature with many capabilities, including: </p> <ul class='spaced-list'> <li>The ability to use variables to reference environment variables, system properties, other config file entries, and a host of other types. - <li>The ability to store and retrieve POJOs as JSON. <li>APIs for updating, modifying, and saving configuration files without losing comments or formatting. <li>Extensive listener APIs. </ul> @@ -1472,7 +1475,7 @@ <ck>aBoolean</ck> = <cv>true</cv> <cc># An int array</cc> - <ck>anIntArray</ck> = <cv>1,2,3</cv> + <ck>anIntArray</ck> = <cv>[1,2,3]</cv> <cc># A POJO that can be converted from a String</cc> <ck>aURL</ck> = <cv>http://foo </cv> @@ -1521,6 +1524,75 @@ String myArg = cf.getString(<js>"MySection/myArg"</js>); String firstArg = cf.getString(<js>"MySection/firstArg"</js>); </p> + <p> + Config files can also be used to directly populate beans using the {@link org.apache.juneau.ini.ConfigFile#getSectionAsBean(String,Class,boolean)}: + </p> + <p class='bcode'> + <jc>// Example config file</jc> + <cs>[MyAddress]</cs> + <ck>name</ck> = <cv>John Smith</cv> + <ck>street</ck> = <cv>123 Main Street</cv> + <ck>city</ck> = <cv>Anywhere</cv> + <ck>state</ck> = <cv>NY</cv> + <ck>zip</ck> = <cv>12345</cv> + + <jc>// Example bean</jc> + <jk>public class</jk> Address { + public String name, street, city; + public StateEnum state; + public int zip; + } + + <jc>// Example usage</jc> + ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); + Address myAddress = cf.getSectionAsBean(<js>"MySection"</js>, Address.<jk>class</jk>); + </p> + <p> + Config file sections can also be accessed via interface proxies using {@link org.apache.juneau.ini.ConfigFile#getSectionAsInterface(String,Class)}: + </p> + <p class='bcode'> + <jc>// Example config file</jc> + <cs>[MySection]</cs> + <ck>string</ck> = <cv>foo</cv> + <ck>int</ck> = <cv>123</cv> + <ck>enum</ck> = <cv>ONE</cv> + <ck>bean</ck> = <cv>{foo:'bar',baz:123}</cv> + <ck>int3dArray</ck> = <cv>[[[123,null],null],null]</cv> + <ck>bean1d3dListMap</ck> = <cv>{key:[[[[{foo:'bar',baz:123}]]]]}</cv> + + <jc>// Example interface</jc> + <jk>public interface</jk> MyConfigInterface { + + String getString(); + <jk>void</jk> setString(String x); + + <jk>int</jk> getInt(); + <jk>void</jk> setInt(<jk>int</jk> x); + + MyEnum getEnum(); + <jk>void</jk> setEnum(MyEnum x); + + MyBean getBean(); + <jk>void</jk> setBean(MyBean x); + + <jk>int</jk>[][][] getInt3dArray(); + <jk>void</jk> setInt3dArray(<jk>int</jk>[][][] x); + + Map<String,List<MyBean[][][]>> getBean1d3dListMap(); + <jk>void</jk> setBean1d3dListMap(Map<String,List<MyBean[][][]>> x); + } + + <jc>// Example usage</jc> + ConfigFile cf = <jk>new</jk> ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>); + MyConfigInterface ci = cf.getSectionAsInterface(<js>"MySection"</js>, MyConfigInterface.<jk>class</jk>); + <jk>int</jk> myInt = ci.getInt(); + ci.setBean(<jk>new</jk> MyBean()); + cf.save(); + </p> + + + + <h6 class='topic'>Additional Information</h6> <ul class='javahierarchy'> @@ -5973,6 +6045,23 @@ <li>New package: {@link org.apache.juneau.http}. <li>Support for dynamic beans. See {@link org.apache.juneau.annotation.BeanProperty#name() @BeanProperty.name()}. <li>New doc: <a class='doclink' href='#Core.JacksonComparison'>2.12 - Comparison with Jackson</a> + <li>All parsers now allow for numeric types with <js>'K'</js>/<js>'M'</js>/<js>'G'</js> suffixes to represent + kilobytes, megabytes, and gigabytes. + <p class='bcode'> + <jc>// Example</jc> + <jk>int</jk> i = JsonParser.<jsf>DEFAULT</jsf>.parse(<js>"123M"</js>); <jc>// 123MB</jc> + </p> + <li>New/modified methods on {@link org.apache.juneau.ini.ConfigFile}: + <ul> + <li>{@link org.apache.juneau.ini.ConfigFile#put(String,String,Object,boolean) put(String,String,Object,boolean)} + <li>{@link org.apache.juneau.ini.ConfigFile#getObject(String,Type,Type...) getObject(String,Type,Type...)} + <li>{@link org.apache.juneau.ini.ConfigFile#getObject(String,Class) getObject(String,Class)} + <li>{@link org.apache.juneau.ini.ConfigFile#getObject(String,String,Type,Type...) getObject(String,String,Type,Type...)} + <li>{@link org.apache.juneau.ini.ConfigFile#getObject(String,String,Class) getObject(String,String,Class)} + <li>{@link org.apache.juneau.ini.ConfigFile#getObjectWithDefault(String,Object,Type,Type...) getObjectWithDefault(String,Object,Type,Type)} + <li>{@link org.apache.juneau.ini.ConfigFile#getObjectWithDefault(String,Object,Class) getObjectWithDefault(String,Object,Class)} + </ul> + <li>New ability to interact with config file sections with proxy interfaces with new method {@link org.apache.juneau.ini.ConfigFile#getSectionAsInterface(String,Class)}. </ul> <h6 class='topic'>org.apache.juneau.rest</h6> http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-examples-rest/examples.cfg ---------------------------------------------------------------------- diff --git a/juneau-examples-rest/examples.cfg b/juneau-examples-rest/examples.cfg index e41894e..352533f 100755 --- a/juneau-examples-rest/examples.cfg +++ b/juneau-examples-rest/examples.cfg @@ -23,10 +23,10 @@ resources = org.apache.juneau.examples.rest.RootResources -# Ports to try. +# Array of ports to try. # 0 means try a random port. # 3 0's means try 3 random ports. -port = 10000, 0, 0, 0 +port = [10000, 0, 0, 0] # Authentication: NONE, BASIC. authType = NONE http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-microservice-template/microservice.cfg ---------------------------------------------------------------------- diff --git a/juneau-microservice-template/microservice.cfg b/juneau-microservice-template/microservice.cfg index 82ac437..c3eb1d0 100755 --- a/juneau-microservice-template/microservice.cfg +++ b/juneau-microservice-template/microservice.cfg @@ -27,9 +27,10 @@ REST = org.apache.juneau.microservice.rest.RestApplication #================================================================================ [REST] -# The HTTP port number to use. -# Default is Rest-Port setting in manifest file, or 8000. -port = 10000 +# Array of ports to try for hosting the REST interface. +# 0 means try a random port. +# 3 0's means try 3 random ports. +port = [10000, 0, 0, 0] # A JSON map of servlet paths to servlet classes. # Example: http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-microservice/src/main/java/org/apache/juneau/microservice/RestMicroservice.java ---------------------------------------------------------------------- diff --git a/juneau-microservice/src/main/java/org/apache/juneau/microservice/RestMicroservice.java b/juneau-microservice/src/main/java/org/apache/juneau/microservice/RestMicroservice.java index 050ec1a..9c05035 100755 --- a/juneau-microservice/src/main/java/org/apache/juneau/microservice/RestMicroservice.java +++ b/juneau-microservice/src/main/java/org/apache/juneau/microservice/RestMicroservice.java @@ -269,7 +269,7 @@ public class RestMicroservice extends Microservice { ch.setFormatter(new LogEntryFormatter(format, dateFormat, false)); logger.addHandler(ch); } - ObjectMap loggerLevels = cf.getObject(ObjectMap.class, "Logging/levels"); + ObjectMap loggerLevels = cf.getObject("Logging/levels", ObjectMap.class); if (loggerLevels != null) for (String l : loggerLevels.keySet()) Logger.getLogger(l).setLevel(loggerLevels.get(Level.class, l)); @@ -339,7 +339,7 @@ public class RestMicroservice extends Microservice { ConfigFile cf = getConfig(); ObjectMap mf = getManifest(); - int[] ports = cf.getObject(int[].class, "REST/port", mf.get(int[].class, "Rest-Port", new int[]{8000})); + int[] ports = cf.getObjectWithDefault("REST/port", mf.get(int[].class, "Rest-Port", new int[]{8000}), int[].class); port = findOpenPort(ports); if (port == 0) { @@ -474,7 +474,7 @@ public class RestMicroservice extends Microservice { ObjectMap mf = getManifest(); Map<String,Class<? extends Servlet>> rm = new LinkedHashMap<String,Class<? extends Servlet>>(); - ObjectMap resourceMap = cf.getObject(ObjectMap.class, "REST/resourceMap"); + ObjectMap resourceMap = cf.getObject("REST/resourceMap", ObjectMap.class); String[] resources = cf.getStringArray("REST/resources", mf.getStringArray("Rest-Resources")); if (resourceMap != null && ! resourceMap.isEmpty()) { http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-rest-test/juneau-rest-test.cfg ---------------------------------------------------------------------- diff --git a/juneau-rest-test/juneau-rest-test.cfg b/juneau-rest-test/juneau-rest-test.cfg index e6dd8f3..ec9628a 100644 --- a/juneau-rest-test/juneau-rest-test.cfg +++ b/juneau-rest-test/juneau-rest-test.cfg @@ -26,7 +26,7 @@ resources = org.apache.juneau.rest.test.Root # Ports to try. # 0 means try a random port. # 3 0's means try 3 random ports. -port = 10001, 0, 0, 0 +port = [10001, 0, 0, 0] # Authentication: NONE, BASIC. authType = NONE @@ -104,11 +104,11 @@ consoleLevel = WARNING [Test] int1 = 1 -int2 = 1,2,3 +int2 = [1,2,3] int3 = $C{Test/int1, -1} int4 = $C{Test/int3, -1} int5 = $C{XXX, -1} boolean1 = true -boolean2 = true,true +boolean2 = [true,true] path = $E{PATH} testManifestEntry = $MF{Test-Entry} http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ConfigResource.java ---------------------------------------------------------------------- diff --git a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ConfigResource.java b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ConfigResource.java index e8f84cc..d766290 100644 --- a/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ConfigResource.java +++ b/juneau-rest-test/src/main/java/org/apache/juneau/rest/test/ConfigResource.java @@ -33,6 +33,6 @@ public class ConfigResource extends Resource { @RestMethod(name="GET", path="/{key}/{class}") public Object test2(RestRequest req, @Path("key") String key, @Path("class") Class<?> c) throws Exception { - return req.getConfigFile().getObject(c, key); + return req.getConfigFile().getObject(key, c); } } http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ConfigTest.java ---------------------------------------------------------------------- diff --git a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ConfigTest.java b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ConfigTest.java index 22ebaff..d52425d 100644 --- a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ConfigTest.java +++ b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/ConfigTest.java @@ -33,17 +33,16 @@ public class ConfigTest extends RestTestcase { ConfigFile cf = c.doGet(URL).getResponse(ConfigFileImpl.class); - assertObjectEquals("{int1:'1',int2:'1,2,3',int3:'$C{Test/int1, -1}',int4:'$C{Test/int3, -1}',int5:'$C{XXX, -1}',boolean1:'true',boolean2:'true,true',path:'$E{PATH}',testManifestEntry:'$MF{Test-Entry}'}", cf.get("Test")); + assertObjectEquals("{int1:'1',int2:'[1,2,3]',int3:'$C{Test/int1, -1}',int4:'$C{Test/int3, -1}',int5:'$C{XXX, -1}',boolean1:'true',boolean2:'[true,true]',path:'$E{PATH}',testManifestEntry:'$MF{Test-Entry}'}", cf.get("Test")); assertEquals("'1'", c.doGet(URL + "/Test%2Fint1/" + getName(String.class)).getResponseAsString()); - assertEquals("['1']", c.doGet(URL + "/Test%2Fint1/" + getName(String[].class)).getResponseAsString()); - assertEquals("'1,2,3'", c.doGet(URL + "/Test%2Fint2/" + getName(String.class)).getResponseAsString()); + assertEquals("'[1,2,3]'", c.doGet(URL + "/Test%2Fint2/" + getName(String.class)).getResponseAsString()); assertEquals("['1','2','3']", c.doGet(URL + "/Test%2Fint2/" + getName(String[].class)).getResponseAsString()); assertEquals("[1,2,3]", c.doGet(URL + "/Test%2Fint2/" + getName(int[].class)).getResponseAsString()); assertEquals("[1,2,3]", c.doGet(URL + "/Test%2Fint2/" + getName(Integer[].class)).getResponseAsString()); - assertEquals("[1]", c.doGet(URL + "/Test%2Fint3/" + getName(int[].class)).getResponseAsString()); - assertEquals("[1]", c.doGet(URL + "/Test%2Fint4/" + getName(int[].class)).getResponseAsString()); - assertEquals("[-1]", c.doGet(URL + "/Test%2Fint5/" + getName(int[].class)).getResponseAsString()); + assertEquals("1", c.doGet(URL + "/Test%2Fint3/" + getName(Integer.class)).getResponseAsString()); + assertEquals("1", c.doGet(URL + "/Test%2Fint4/" + getName(Integer.class)).getResponseAsString()); + assertEquals("-1", c.doGet(URL + "/Test%2Fint5/" + getName(Integer.class)).getResponseAsString()); assertEquals("true", c.doGet(URL + "/Test%2Fboolean1/" + getName(Boolean.class)).getResponseAsString()); assertEquals("[true,true]", c.doGet(URL + "/Test%2Fboolean2/" + getName(Boolean[].class)).getResponseAsString()); assertTrue(c.doGet(URL + "/Test%2Fpath/" + getName(String.class)).getResponseAsString().length() > 10); http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ca59d8a4/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/_TestSuite.java ---------------------------------------------------------------------- diff --git a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/_TestSuite.java b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/_TestSuite.java index d8a0b1b..eb2f247 100644 --- a/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/_TestSuite.java +++ b/juneau-rest-test/src/test/java/org/apache/juneau/rest/test/_TestSuite.java @@ -38,6 +38,7 @@ import org.junit.runners.Suite.*; GroupsTest.class, GzipTest.class, HeadersTest.class, + HtmlPropertiesTest.class, InheritanceTest.class, InterfaceProxyTest.class, JacocoDummyTest.class, @@ -54,6 +55,7 @@ import org.junit.runners.Suite.*; ParsersTest.class, PathsTest.class, PathTest.class, + PathVariableTest.class, PropertiesTest.class, QueryTest.class, RestClientTest.class,
