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.FileInputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.InputStreamReader;
027import java.io.PrintWriter;
028import java.io.Reader;
029import java.io.StringWriter;
030import java.net.URISyntaxException;
031import java.net.URL;
032import java.nio.charset.Charset;
033import java.nio.charset.StandardCharsets;
034import java.util.Map;
035import java.util.Objects;
036import java.util.concurrent.Executor;
037import java.util.concurrent.atomic.AtomicLong;
038
039import javax.management.MBeanNotificationInfo;
040import javax.management.Notification;
041import javax.management.NotificationBroadcasterSupport;
042import javax.management.ObjectName;
043
044import org.apache.logging.log4j.core.LoggerContext;
045import org.apache.logging.log4j.core.config.Configuration;
046import org.apache.logging.log4j.core.config.ConfigurationFactory;
047import org.apache.logging.log4j.core.config.ConfigurationSource;
048import org.apache.logging.log4j.core.util.Closer;
049import org.apache.logging.log4j.status.StatusLogger;
050import org.apache.logging.log4j.util.Strings;
051
052/**
053 * Implementation of the {@code LoggerContextAdminMBean} interface.
054 */
055public class LoggerContextAdmin extends NotificationBroadcasterSupport implements LoggerContextAdminMBean,
056        PropertyChangeListener {
057    private static final int PAGE = 4 * 1024;
058    private static final int TEXT_BUFFER = 64 * 1024;
059    private static final int BUFFER_SIZE = 2048;
060    private static final StatusLogger LOGGER = StatusLogger.getLogger();
061
062    private final AtomicLong sequenceNo = new AtomicLong();
063    private final ObjectName objectName;
064    private final LoggerContext loggerContext;
065
066    /**
067     * Constructs a new {@code LoggerContextAdmin} with the {@code Executor} to be used for sending {@code Notification}
068     * 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 = Objects.requireNonNull(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[] { NOTIF_TYPE_RECONFIGURED };
088        final String name = Notification.class.getName();
089        final String description = "Configuration reconfigured";
090        return new MBeanNotificationInfo(notifTypes, name, description);
091    }
092
093    @Override
094    public String getStatus() {
095        return loggerContext.getState().toString();
096    }
097
098    @Override
099    public String getName() {
100        return loggerContext.getName();
101    }
102
103    private Configuration getConfig() {
104        return loggerContext.getConfiguration();
105    }
106
107    @Override
108    public String getConfigLocationUri() {
109        if (loggerContext.getConfigLocation() != null) {
110            return String.valueOf(loggerContext.getConfigLocation());
111        }
112        if (getConfigName() != null) {
113            return String.valueOf(new File(getConfigName()).toURI());
114        }
115        return Strings.EMPTY;
116    }
117
118    @Override
119    public void setConfigLocationUri(final String configLocation) throws URISyntaxException, IOException {
120        if (configLocation == null || configLocation.isEmpty()) {
121            throw new IllegalArgumentException("Missing configuration location");
122        }
123        LOGGER.debug("---------");
124        LOGGER.debug("Remote request to reconfigure using location " + configLocation);
125        final File configFile = new File(configLocation);
126        ConfigurationSource configSource = null;
127        if (configFile.exists()) {
128            LOGGER.debug("Opening config file {}", configFile.getAbsolutePath());
129            configSource = new ConfigurationSource(new FileInputStream(configFile), configFile);
130        } else {
131            final URL configURL = new URL(configLocation);
132            LOGGER.debug("Opening config URL {}", configURL);
133            configSource = new ConfigurationSource(configURL.openStream(), configURL);
134        }
135        final Configuration config = ConfigurationFactory.getInstance().getConfiguration(loggerContext, configSource);
136        loggerContext.start(config);
137        LOGGER.debug("Completed remote request to reconfigure.");
138    }
139
140    @Override
141    public void propertyChange(final PropertyChangeEvent evt) {
142        if (!LoggerContext.PROPERTY_CONFIG.equals(evt.getPropertyName())) {
143            return;
144        }
145        final Notification notif = new Notification(NOTIF_TYPE_RECONFIGURED, getObjectName(), nextSeqNo(), now(), null);
146        sendNotification(notif);
147    }
148
149    @Override
150    public String getConfigText() throws IOException {
151        return getConfigText(StandardCharsets.UTF_8.name());
152    }
153
154    @Override
155    public String getConfigText(final String charsetName) throws IOException {
156        try {
157            final ConfigurationSource source = loggerContext.getConfiguration().getConfigurationSource();
158            final ConfigurationSource copy = source.resetInputStream();
159            final Charset charset = Charset.forName(charsetName);
160            return readContents(copy.getInputStream(), charset);
161        } catch (final Exception ex) {
162            final StringWriter sw = new StringWriter(BUFFER_SIZE);
163            ex.printStackTrace(new PrintWriter(sw));
164            return sw.toString();
165        }
166    }
167
168    /**
169     * Returns the contents of the specified input stream as a String.
170     * @param in stream to read from
171     * @param charset MUST not be null
172     * @return stream contents
173     * @throws IOException if a problem occurred reading from the stream.
174     */
175    private String readContents(final InputStream in, final Charset charset) throws IOException {
176        Reader reader = null;
177        try {
178            reader = new InputStreamReader(in, charset);
179            final StringBuilder result = new StringBuilder(TEXT_BUFFER);
180            final char[] buff = new char[PAGE];
181            int count = -1;
182            while ((count = reader.read(buff)) >= 0) {
183                result.append(buff, 0, count);
184            }
185            return result.toString();
186        } finally {
187            Closer.closeSilently(in);
188            Closer.closeSilently(reader);
189        }
190    }
191
192    @Override
193    public void setConfigText(final String configText, final String charsetName) {
194        LOGGER.debug("---------");
195        LOGGER.debug("Remote request to reconfigure from config text.");
196
197        try {
198            final InputStream in = new ByteArrayInputStream(configText.getBytes(charsetName));
199            final ConfigurationSource source = new ConfigurationSource(in);
200            final Configuration updated = ConfigurationFactory.getInstance().getConfiguration(loggerContext, source);
201            loggerContext.start(updated);
202            LOGGER.debug("Completed remote request to reconfigure from config text.");
203        } catch (final Exception ex) {
204            final String msg = "Could not reconfigure from config text";
205            LOGGER.error(msg, ex);
206            throw new IllegalArgumentException(msg, ex);
207        }
208    }
209
210    @Override
211    public String getConfigName() {
212        return getConfig().getName();
213    }
214
215    @Override
216    public String getConfigClassName() {
217        return getConfig().getClass().getName();
218    }
219
220    @Override
221    public String getConfigFilter() {
222        return String.valueOf(getConfig().getFilter());
223    }
224
225    @Override
226    public Map<String, String> getConfigProperties() {
227        return getConfig().getProperties();
228    }
229
230    /**
231     * Returns the {@code ObjectName} of this mbean.
232     *
233     * @return the {@code ObjectName}
234     * @see LoggerContextAdminMBean#PATTERN
235     */
236    @Override
237    public ObjectName getObjectName() {
238        return objectName;
239    }
240
241    private long nextSeqNo() {
242        return sequenceNo.getAndIncrement();
243    }
244
245    private long now() {
246        return System.currentTimeMillis();
247    }
248}