001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018// Contributors:  Georg Lundesgaard
019
020package org.apache.log4j.config;
021
022import java.beans.BeanInfo;
023import java.beans.IntrospectionException;
024import java.beans.Introspector;
025import java.beans.PropertyDescriptor;
026import java.io.InterruptedIOException;
027import java.lang.reflect.InvocationTargetException;
028import java.lang.reflect.Method;
029import java.util.Properties;
030
031import org.apache.log4j.Appender;
032import org.apache.log4j.Level;
033import org.apache.log4j.Priority;
034import org.apache.log4j.spi.ErrorHandler;
035import org.apache.log4j.spi.OptionHandler;
036import org.apache.logging.log4j.Logger;
037import org.apache.logging.log4j.core.util.OptionConverter;
038import org.apache.logging.log4j.status.StatusLogger;
039
040/**
041 * General purpose Object property setter. Clients repeatedly invokes
042 * {@link #setProperty setProperty(name,value)} in order to invoke setters
043 * on the Object specified in the constructor. This class relies on the
044 * JavaBeans {@link Introspector} to analyze the given Object Class using
045 * reflection.
046 *
047 * <p>Usage:
048 * <pre>
049 * PropertySetter ps = new PropertySetter(anObject);
050 * ps.set("name", "Joe");
051 * ps.set("age", "32");
052 * ps.set("isMale", "true");
053 * </pre>
054 * will cause the invocations anObject.setName("Joe"), anObject.setAge(32),
055 * and setMale(true) if such methods exist with those signatures.
056 * Otherwise an {@link IntrospectionException} are thrown.
057 */
058public class PropertySetter {
059    private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {};
060    private static Logger LOGGER = StatusLogger.getLogger();
061    protected Object obj;
062    protected PropertyDescriptor[] props;
063
064    /**
065     * Create a new PropertySetter for the specified Object. This is done
066     * in prepartion for invoking {@link #setProperty} one or more times.
067     *
068     * @param obj the object for which to set properties
069     */
070    public PropertySetter(Object obj) {
071        this.obj = obj;
072    }
073
074    /**
075     * Set the properties of an object passed as a parameter in one
076     * go. The <code>properties</code> are parsed relative to a
077     * <code>prefix</code>.
078     *
079     * @param obj        The object to configure.
080     * @param properties A java.util.Properties containing keys and values.
081     * @param prefix     Only keys having the specified prefix will be set.
082     */
083    public static void setProperties(Object obj, Properties properties, String prefix) {
084        new PropertySetter(obj).setProperties(properties, prefix);
085    }
086
087    /**
088     * Uses JavaBeans {@link Introspector} to computer setters of object to be
089     * configured.
090     */
091    protected void introspect() {
092        try {
093            BeanInfo bi = Introspector.getBeanInfo(obj.getClass());
094            props = bi.getPropertyDescriptors();
095        } catch (IntrospectionException ex) {
096            LOGGER.error("Failed to introspect {}: {}", obj, ex.getMessage());
097            props = EMPTY_PROPERTY_DESCRIPTOR_ARRAY;
098        }
099    }
100
101    /**
102     * Set the properites for the object that match the
103     * <code>prefix</code> passed as parameter.
104     * @param properties The properties.
105     * @param prefix The prefix of the properties to use.
106     */
107    public void setProperties(Properties properties, String prefix) {
108        int len = prefix.length();
109
110        for (String key : properties.stringPropertyNames()) {
111
112            // handle only properties that start with the desired prefix.
113            if (key.startsWith(prefix)) {
114
115
116                // ignore key if it contains dots after the prefix
117                if (key.indexOf('.', len + 1) > 0) {
118                    continue;
119                }
120
121                String value = OptionConverter.findAndSubst(key, properties);
122                key = key.substring(len);
123                if (("layout".equals(key) || "errorhandler".equals(key)) && obj instanceof Appender) {
124                    continue;
125                }
126                //
127                //   if the property type is an OptionHandler
128                //     (for example, triggeringPolicy of org.apache.log4j.rolling.RollingFileAppender)
129                PropertyDescriptor prop = getPropertyDescriptor(Introspector.decapitalize(key));
130                if (prop != null
131                        && OptionHandler.class.isAssignableFrom(prop.getPropertyType())
132                        && prop.getWriteMethod() != null) {
133                    OptionHandler opt = (OptionHandler)
134                            OptionConverter.instantiateByKey(properties, prefix + key,
135                                    prop.getPropertyType(),
136                                    null);
137                    PropertySetter setter = new PropertySetter(opt);
138                    setter.setProperties(properties, prefix + key + ".");
139                    try {
140                        prop.getWriteMethod().invoke(this.obj, opt);
141                    } catch (InvocationTargetException ex) {
142                        if (ex.getTargetException() instanceof InterruptedException
143                                || ex.getTargetException() instanceof InterruptedIOException) {
144                            Thread.currentThread().interrupt();
145                        }
146                        LOGGER.warn("Failed to set property [{}] to value \"{}\".", key, value, ex);
147                    } catch (IllegalAccessException | RuntimeException ex) {
148                        LOGGER.warn("Failed to set property [{}] to value \"{}\".", key, value, ex);
149                    }
150                    continue;
151                }
152
153                setProperty(key, value);
154            }
155        }
156        activate();
157    }
158
159    /**
160     * Set a property on this PropertySetter's Object. If successful, this
161     * method will invoke a setter method on the underlying Object. The
162     * setter is the one for the specified property name and the value is
163     * determined partly from the setter argument type and partly from the
164     * value specified in the call to this method.
165     *
166     * <p>If the setter expects a String no conversion is necessary.
167     * If it expects an int, then an attempt is made to convert 'value'
168     * to an int using new Integer(value). If the setter expects a boolean,
169     * the conversion is by new Boolean(value).
170     *
171     * @param name  name of the property
172     * @param value String value of the property
173     */
174    public void setProperty(String name, String value) {
175        if (value == null) {
176            return;
177        }
178
179        name = Introspector.decapitalize(name);
180        PropertyDescriptor prop = getPropertyDescriptor(name);
181
182        //LOGGER.debug("---------Key: "+name+", type="+prop.getPropertyType());
183
184        if (prop == null) {
185            LOGGER.warn("No such property [" + name + "] in " +
186                    obj.getClass().getName() + ".");
187        } else {
188            try {
189                setProperty(prop, name, value);
190            } catch (PropertySetterException ex) {
191                LOGGER.warn("Failed to set property [{}] to value \"{}\".", name, value, ex.rootCause);
192            }
193        }
194    }
195
196    /**
197     * Set the named property given a {@link PropertyDescriptor}.
198     *
199     * @param prop  A PropertyDescriptor describing the characteristics
200     *              of the property to set.
201     * @param name  The named of the property to set.
202     * @param value The value of the property.
203     * @throws PropertySetterException if no setter is available.
204     */
205    public void setProperty(PropertyDescriptor prop, String name, String value)
206            throws PropertySetterException {
207        Method setter = prop.getWriteMethod();
208        if (setter == null) {
209            throw new PropertySetterException("No setter for property [" + name + "].");
210        }
211        Class<?>[] paramTypes = setter.getParameterTypes();
212        if (paramTypes.length != 1) {
213            throw new PropertySetterException("#params for setter != 1");
214        }
215
216        Object arg;
217        try {
218            arg = convertArg(value, paramTypes[0]);
219        } catch (Throwable t) {
220            throw new PropertySetterException("Conversion to type [" + paramTypes[0] +
221                    "] failed. Reason: " + t);
222        }
223        if (arg == null) {
224            throw new PropertySetterException(
225                    "Conversion to type [" + paramTypes[0] + "] failed.");
226        }
227        LOGGER.debug("Setting property [" + name + "] to [" + arg + "].");
228        try {
229            setter.invoke(obj, arg);
230        } catch (InvocationTargetException ex) {
231            if (ex.getTargetException() instanceof InterruptedException
232                    || ex.getTargetException() instanceof InterruptedIOException) {
233                Thread.currentThread().interrupt();
234            }
235            throw new PropertySetterException(ex);
236        } catch (IllegalAccessException | RuntimeException ex) {
237            throw new PropertySetterException(ex);
238        }
239    }
240
241
242    /**
243     * Convert <code>val</code> a String parameter to an object of a
244     * given type.
245     * @param val The value to convert.
246     * @param type The type of the value to convert to.
247     * @return The result of the conversion.
248     */
249    protected Object convertArg(String val, Class<?> type) {
250        if (val == null) {
251            return null;
252        }
253
254        String v = val.trim();
255        if (String.class.isAssignableFrom(type)) {
256            return val;
257        } else if (Integer.TYPE.isAssignableFrom(type)) {
258            return Integer.parseInt(v);
259        } else if (Long.TYPE.isAssignableFrom(type)) {
260            return Long.parseLong(v);
261        } else if (Boolean.TYPE.isAssignableFrom(type)) {
262            if ("true".equalsIgnoreCase(v)) {
263                return Boolean.TRUE;
264            } else if ("false".equalsIgnoreCase(v)) {
265                return Boolean.FALSE;
266            }
267        } else if (Priority.class.isAssignableFrom(type)) {
268            return org.apache.log4j.helpers.OptionConverter.toLevel(v, Level.DEBUG);
269        } else if (ErrorHandler.class.isAssignableFrom(type)) {
270            return OptionConverter.instantiateByClassName(v,
271                    ErrorHandler.class, null);
272        }
273        return null;
274    }
275
276
277    protected PropertyDescriptor getPropertyDescriptor(String name) {
278        if (props == null) {
279            introspect();
280        }
281        for (PropertyDescriptor prop : props) {
282            if (name.equals(prop.getName())) {
283                return prop;
284            }
285        }
286        return null;
287    }
288
289    public void activate() {
290        if (obj instanceof OptionHandler) {
291            ((OptionHandler) obj).activateOptions();
292        }
293    }
294}