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
018package org.apache.logging.log4j.core.config;
019
020import java.io.ByteArrayInputStream;
021import java.io.ByteArrayOutputStream;
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileNotFoundException;
025import java.io.IOException;
026import java.io.InputStream;
027import java.net.MalformedURLException;
028import java.net.URI;
029import java.net.URISyntaxException;
030import java.net.URL;
031import java.net.URLConnection;
032import java.util.Objects;
033
034import org.apache.logging.log4j.Level;
035import org.apache.logging.log4j.core.net.UrlConnectionFactory;
036import org.apache.logging.log4j.core.util.FileUtils;
037import org.apache.logging.log4j.core.util.Loader;
038import org.apache.logging.log4j.core.util.Source;
039import org.apache.logging.log4j.util.LoaderUtil;
040
041/**
042 * Represents the source for the logging configuration.
043 */
044public class ConfigurationSource {
045
046    /**
047     * ConfigurationSource to use with Configurations that do not require a "real" configuration source.
048     */
049    public static final ConfigurationSource NULL_SOURCE = new ConfigurationSource(new byte[0], null, 0);
050    private static final String HTTPS = "https";
051    private static final String HTTP = "http";
052
053    private final File file;
054    private final URL url;
055    private final String location;
056    private final InputStream stream;
057    private volatile byte[] data;
058    private volatile Source source = null;
059    private final long lastModified;
060    // Set when the configuration has been updated so reset can use it for the next lastModified timestamp.
061    private volatile long modifiedMillis;
062
063    /**
064     * Constructs a new {@code ConfigurationSource} with the specified input stream that originated from the specified
065     * file.
066     *
067     * @param stream the input stream
068     * @param file the file where the input stream originated
069     */
070    public ConfigurationSource(final InputStream stream, final File file) {
071        this.stream = Objects.requireNonNull(stream, "stream is null");
072        this.file = Objects.requireNonNull(file, "file is null");
073        this.location = file.getAbsolutePath();
074        this.url = null;
075        this.data = null;
076        long modified = 0;
077        try {
078            modified = file.lastModified();
079        } catch (Exception ex) {
080            // There is a problem with the file. It will be handled somewhere else.
081        }
082        this.lastModified = modified;
083    }
084
085    /**
086     * Constructs a new {@code ConfigurationSource} with the specified input stream that originated from the specified
087     * url.
088     *
089     * @param stream the input stream
090     * @param url the URL where the input stream originated
091     */
092    public ConfigurationSource(final InputStream stream, final URL url) {
093        this.stream = Objects.requireNonNull(stream, "stream is null");
094        this.url = Objects.requireNonNull(url, "URL is null");
095        this.location = url.toString();
096        this.file = null;
097        this.data = null;
098        this.lastModified = 0;
099    }
100
101    /**
102     * Constructs a new {@code ConfigurationSource} with the specified input stream that originated from the specified
103     * url.
104     *
105     * @param stream the input stream
106     * @param url the URL where the input stream originated
107     * @param lastModified when the source was last modified.
108     */
109    public ConfigurationSource(final InputStream stream, final URL url, long lastModified) {
110        this.stream = Objects.requireNonNull(stream, "stream is null");
111        this.url = Objects.requireNonNull(url, "URL is null");
112        this.location = url.toString();
113        this.file = null;
114        this.data = null;
115        this.lastModified = lastModified;
116    }
117
118    /**
119     * Constructs a new {@code ConfigurationSource} with the specified input stream. Since the stream is the only source
120     * of data, this constructor makes a copy of the stream contents.
121     *
122     * @param stream the input stream
123     * @throws IOException if an exception occurred reading from the specified stream
124     */
125    public ConfigurationSource(final InputStream stream) throws IOException {
126        this(toByteArray(stream), null, 0);
127    }
128
129    public ConfigurationSource(final Source source, final byte[] data, long lastModified) throws IOException {
130        Objects.requireNonNull(source, "source is null");
131        this.data = Objects.requireNonNull(data, "data is null");
132        this.stream = new ByteArrayInputStream(data);
133        this.file = source.getFile();
134        this.url = source.getURI().toURL();
135        this.location = source.getLocation();
136        this.lastModified = lastModified;
137    }
138
139    private ConfigurationSource(final byte[] data, final URL url, long lastModified) {
140        Objects.requireNonNull(data, "data is null");
141        this.stream = new ByteArrayInputStream(data);
142        this.file = null;
143        this.url = url;
144        this.location = null;
145        this.lastModified = lastModified;
146    }
147
148    /**
149     * Returns the contents of the specified {@code InputStream} as a byte array.
150     *
151     * @param inputStream the stream to read
152     * @return the contents of the specified stream
153     * @throws IOException if a problem occurred reading from the stream
154     */
155    private static byte[] toByteArray(final InputStream inputStream) throws IOException {
156        final int buffSize = Math.max(4096, inputStream.available());
157        final ByteArrayOutputStream contents = new ByteArrayOutputStream(buffSize);
158        final byte[] buff = new byte[buffSize];
159
160        int length = inputStream.read(buff);
161        while (length > 0) {
162            contents.write(buff, 0, length);
163            length = inputStream.read(buff);
164        }
165        return contents.toByteArray();
166    }
167
168    /**
169     * Returns the file configuration source, or {@code null} if this configuration source is based on an URL or has
170     * neither a file nor an URL.
171     *
172     * @return the configuration source file, or {@code null}
173     */
174    public File getFile() {
175        return file;
176    }
177
178    /**
179     * Returns the configuration source URL, or {@code null} if this configuration source is based on a file or has
180     * neither a file nor an URL.
181     *
182     * @return the configuration source URL, or {@code null}
183     */
184    public URL getURL() {
185        return url;
186    }
187
188    public void setSource(Source source) {
189        this.source = source;
190    }
191
192    public void setData(byte[] data) {
193        this.data = data;
194    }
195
196    public void setModifiedMillis(long modifiedMillis) {
197        this.modifiedMillis = modifiedMillis;
198    }
199
200    /**
201     * Returns a URI representing the configuration resource or null if it cannot be determined.
202     * @return The URI.
203     */
204    public URI getURI() {
205        URI sourceURI = null;
206        if (url != null) {
207            try {
208                sourceURI = url.toURI();
209            } catch (final URISyntaxException ex) {
210                    /* Ignore the exception */
211            }
212        }
213        if (sourceURI == null && file != null) {
214            sourceURI = file.toURI();
215        }
216        if (sourceURI == null && location != null) {
217            try {
218                sourceURI = new URI(location);
219            } catch (final URISyntaxException ex) {
220                // Assume the scheme was missing.
221                try {
222                    sourceURI = new URI("file://" + location);
223                } catch (final URISyntaxException uriEx) {
224                    /* Ignore the exception */
225                }
226            }
227        }
228        return sourceURI;
229    }
230
231    /**
232     * Returns the time the resource was last modified or 0 if it is not available.
233     * @return the last modified time of the resource.
234     */
235    public long getLastModified() {
236        return lastModified;
237    }
238
239    /**
240     * Returns a string describing the configuration source file or URL, or {@code null} if this configuration source
241     * has neither a file nor an URL.
242     *
243     * @return a string describing the configuration source file or URL, or {@code null}
244     */
245    public String getLocation() {
246        return location;
247    }
248
249    /**
250     * Returns the input stream that this configuration source was constructed with.
251     *
252     * @return the input stream that this configuration source was constructed with.
253     */
254    public InputStream getInputStream() {
255        return stream;
256    }
257
258    /**
259     * Returns a new {@code ConfigurationSource} whose input stream is reset to the beginning.
260     *
261     * @return a new {@code ConfigurationSource}
262     * @throws IOException if a problem occurred while opening the new input stream
263     */
264    public ConfigurationSource resetInputStream() throws IOException {
265        if (source != null) {
266            return new ConfigurationSource(source, data, this.lastModified);
267        } else if (file != null) {
268            return new ConfigurationSource(new FileInputStream(file), file);
269        } else if (url != null && data != null) {
270            // Creates a ConfigurationSource without accessing the URL since the data was provided.
271            return new ConfigurationSource(data, url, modifiedMillis == 0 ? lastModified : modifiedMillis);
272        } else if (url != null) {
273            return fromUri(getURI());
274        } else if (data != null) {
275            return new ConfigurationSource(data, null, lastModified);
276        }
277        return null;
278    }
279
280    @Override
281    public String toString() {
282        if (location != null) {
283            return location;
284        }
285        if (this == NULL_SOURCE) {
286            return "NULL_SOURCE";
287        }
288        final int length = data == null ? -1 : data.length;
289        return "stream (" + length + " bytes, unknown location)";
290    }
291
292    /**
293     * Loads the configuration from a URI.
294     * @param configLocation A URI representing the location of the configuration.
295     * @return The ConfigurationSource for the configuration.
296     */
297    public static ConfigurationSource fromUri(final URI configLocation) {
298        final File configFile = FileUtils.fileFromUri(configLocation);
299        if (configFile != null && configFile.exists() && configFile.canRead()) {
300            try {
301                return new ConfigurationSource(new FileInputStream(configFile), configFile);
302            } catch (final FileNotFoundException ex) {
303                ConfigurationFactory.LOGGER.error("Cannot locate file {}", configLocation.getPath(), ex);
304            }
305        }
306        if (ConfigurationFactory.isClassLoaderUri(configLocation)) {
307            final ClassLoader loader = LoaderUtil.getThreadContextClassLoader();
308            final String path = ConfigurationFactory.extractClassLoaderUriPath(configLocation);
309            final ConfigurationSource source = fromResource(path, loader);
310            if (source != null) {
311                return source;
312            }
313        }
314        if (!configLocation.isAbsolute()) { // LOG4J2-704 avoid confusing error message thrown by uri.toURL()
315            ConfigurationFactory.LOGGER.error("File not found in file system or classpath: {}", configLocation.toString());
316            return null;
317        }
318        try {
319            URL url = configLocation.toURL();
320            URLConnection urlConnection = UrlConnectionFactory.createConnection(url);
321            InputStream is = urlConnection.getInputStream();
322            long lastModified = urlConnection.getLastModified();
323            return new ConfigurationSource(is, configLocation.toURL(), lastModified);
324        } catch (final MalformedURLException ex) {
325            ConfigurationFactory.LOGGER.error("Invalid URL {}", configLocation.toString(), ex);
326        } catch (final Exception ex) {
327            ConfigurationFactory.LOGGER.error("Unable to access {}", configLocation.toString(), ex);
328        }
329        return null;
330    }
331
332    /**
333     * Retrieves the configuration via the ClassLoader.
334     * @param resource The resource to load.
335     * @param loader The default ClassLoader to use.
336     * @return The ConfigurationSource for the configuration.
337     */
338    public static ConfigurationSource fromResource(final String resource, final ClassLoader loader) {
339        final URL url = Loader.getResource(resource, loader);
340        if (url == null) {
341            return null;
342        }
343        InputStream is = null;
344        try {
345            is = url.openStream();
346        } catch (final IOException ioe) {
347            ConfigurationFactory.LOGGER.catching(Level.DEBUG, ioe);
348            return null;
349        }
350        if (is == null) {
351            return null;
352        }
353
354        if (FileUtils.isFile(url)) {
355            try {
356                return new ConfigurationSource(is, FileUtils.fileFromUri(url.toURI()));
357            } catch (final URISyntaxException ex) {
358                // Just ignore the exception.
359                ConfigurationFactory.LOGGER.catching(Level.DEBUG, ex);
360            }
361        }
362        return new ConfigurationSource(is, url);
363    }
364}