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 */
017package org.apache.logging.log4j.core.jmx;
018
019import java.beans.PropertyChangeEvent;
020import java.beans.PropertyChangeListener;
021import java.io.ByteArrayInputStream;
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.io.PrintWriter;
027import java.io.Reader;
028import java.io.StringWriter;
029import java.net.URI;
030import java.net.URISyntaxException;
031import java.nio.charset.Charset;
032import java.util.Map;
033import java.util.concurrent.Executor;
034import java.util.concurrent.atomic.AtomicLong;
035
036import javax.management.MBeanNotificationInfo;
037import javax.management.Notification;
038import javax.management.NotificationBroadcasterSupport;
039import javax.management.ObjectName;
040
041import org.apache.logging.log4j.core.LoggerContext;
042import org.apache.logging.log4j.core.config.Configuration;
043import org.apache.logging.log4j.core.config.ConfigurationFactory;
044import org.apache.logging.log4j.core.config.ConfigurationFactory.ConfigurationSource;
045import org.apache.logging.log4j.core.helpers.Assert;
046import org.apache.logging.log4j.core.helpers.Charsets;
047import org.apache.logging.log4j.core.helpers.Closer;
048import org.apache.logging.log4j.core.helpers.FileUtils;
049import org.apache.logging.log4j.status.StatusLogger;
050
051/**
052 * Implementation of the {@code LoggerContextAdminMBean} interface.
053 */
054public class LoggerContextAdmin extends NotificationBroadcasterSupport
055        implements LoggerContextAdminMBean, PropertyChangeListener {
056    private static final int PAGE = 4 * 1024;
057    private static final int TEXT_BUFFER = 64 * 1024;
058    private static final int BUFFER_SIZE = 2048;
059    private static final StatusLogger LOGGER = StatusLogger.getLogger();
060
061    private final AtomicLong sequenceNo = new AtomicLong();
062    private final ObjectName objectName;
063    private final LoggerContext loggerContext;
064    private String customConfigText;
065
066    /**
067     * Constructs a new {@code LoggerContextAdmin} with the {@code Executor} to
068     * be used for sending {@code Notification}s asynchronously to listeners.
069     *
070     * @param executor used to send notifications asynchronously
071     * @param loggerContext the instrumented object
072     */
073    public LoggerContextAdmin(final LoggerContext loggerContext, final Executor executor) {
074        super(executor, createNotificationInfo());
075        this.loggerContext = Assert.isNotNull(loggerContext, "loggerContext");
076        try {
077            final String ctxName = Server.escape(loggerContext.getName());
078            final String name = String.format(PATTERN, ctxName);
079            objectName = new ObjectName(name);
080        } catch (final Exception e) {
081            throw new IllegalStateException(e);
082        }
083        loggerContext.addPropertyChangeListener(this);
084    }
085
086    private static MBeanNotificationInfo createNotificationInfo() {
087        final String[] notifTypes = new String[] {//
088                NOTIF_TYPE_RECONFIGURED };
089        final String name = Notification.class.getName();
090        final String description = "Configuration reconfigured";
091        return new MBeanNotificationInfo(notifTypes, name, description);
092    }
093
094    @Override
095    public String getStatus() {
096        return loggerContext.getStatus().toString();
097    }
098
099    @Override
100    public String getName() {
101        return loggerContext.getName();
102    }
103
104    private Configuration getConfig() {
105        return loggerContext.getConfiguration();
106    }
107
108    @Override
109    public String getConfigLocationURI() {
110        if (loggerContext.getConfigLocation() != null) {
111            return String.valueOf(loggerContext.getConfigLocation());
112        }
113        if (getConfigName() != null) {
114            return String.valueOf(new File(getConfigName()).toURI());
115        }
116        return "";
117    }
118
119    @Override
120    public void setConfigLocationURI(final String configLocation)
121            throws URISyntaxException, IOException {
122        LOGGER.debug("---------");
123        LOGGER.debug("Remote request to reconfigure using location "
124                + configLocation);
125        final URI uri = FileUtils.getCorrectedFilePathUri(configLocation);
126
127        // validate the location first: invalid location will result in
128        // default configuration being configured, try to avoid that...
129        uri.toURL().openStream().close();
130
131        loggerContext.setConfigLocation(uri);
132        LOGGER.debug("Completed remote request to reconfigure.");
133    }
134
135    @Override
136    public void propertyChange(final PropertyChangeEvent evt) {
137        if (!LoggerContext.PROPERTY_CONFIG.equals(evt.getPropertyName())) {
138            return;
139        }
140        // erase custom text if new configuration was read from a location
141        if (loggerContext.getConfiguration().getName() != null) {
142            customConfigText = null;
143        }
144        final Notification notif = new Notification(NOTIF_TYPE_RECONFIGURED,
145                getObjectName(), nextSeqNo(), now(), null);
146        sendNotification(notif);
147    }
148
149    @Override
150    public String getConfigText() throws IOException {
151        return getConfigText(Charsets.UTF_8.name());
152    }
153
154    @Override
155    public String getConfigText(final String charsetName) throws IOException {
156        if (customConfigText != null) {
157            return customConfigText;
158        }
159        try {
160            final Charset charset = Charset.forName(charsetName);
161            return readContents(FileUtils.getCorrectedFilePathUri(getConfigLocationURI()), charset);
162        } catch (final Exception ex) {
163            final StringWriter sw = new StringWriter(BUFFER_SIZE);
164            ex.printStackTrace(new PrintWriter(sw));
165            return sw.toString();
166        }
167    }
168
169    @Override
170    public void setConfigText(final String configText, final String charsetName) {
171        final String old = customConfigText;
172        customConfigText = Assert.isNotNull(configText, "configText");
173        LOGGER.debug("---------");
174        LOGGER.debug("Remote request to reconfigure from config text.");
175
176        try {
177            final InputStream in = new ByteArrayInputStream(
178                    configText.getBytes(charsetName));
179            final ConfigurationSource source = new ConfigurationSource(in);
180            final Configuration updated = ConfigurationFactory.getInstance()
181                    .getConfiguration(source);
182            loggerContext.start(updated);
183            LOGGER.debug("Completed remote request to reconfigure from config text.");
184        } catch (final Exception ex) {
185            customConfigText = old;
186            final String msg = "Could not reconfigure from config text";
187            LOGGER.error(msg, ex);
188            throw new IllegalArgumentException(msg, ex);
189        }
190    }
191
192    /**
193     * 
194     * @param uri
195     * @param charset MUST not be null
196     * @return
197     * @throws IOException
198     */
199    private String readContents(final URI uri, final Charset charset) throws IOException {
200        InputStream in = null;
201        Reader reader = null;
202        try {
203            in = uri.toURL().openStream();
204            reader = new InputStreamReader(in, charset);
205            final StringBuilder result = new StringBuilder(TEXT_BUFFER);
206            final char[] buff = new char[PAGE];
207            int count = -1;
208            while ((count = reader.read(buff)) >= 0) {
209                result.append(buff, 0, count);
210            }
211            return result.toString();
212        } finally {
213            Closer.closeSilent(in);
214            Closer.closeSilent(reader);
215        }
216    }
217
218    @Override
219    public String getConfigName() {
220        return getConfig().getName();
221    }
222
223    @Override
224    public String getConfigClassName() {
225        return getConfig().getClass().getName();
226    }
227
228    @Override
229    public String getConfigFilter() {
230        return String.valueOf(getConfig().getFilter());
231    }
232
233    @Override
234    public String getConfigMonitorClassName() {
235        return getConfig().getConfigurationMonitor().getClass().getName();
236    }
237
238    @Override
239    public Map<String, String> getConfigProperties() {
240        return getConfig().getProperties();
241    }
242
243    /**
244     * Returns the {@code ObjectName} of this mbean.
245     *
246     * @return the {@code ObjectName}
247     * @see LoggerContextAdminMBean#PATTERN
248     */
249    @Override
250    public ObjectName getObjectName() {
251        return objectName;
252    }
253
254    private long nextSeqNo() {
255        return sequenceNo.getAndIncrement();
256    }
257
258    private long now() {
259        return System.currentTimeMillis();
260    }
261}