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.config.json;
018
019import java.io.ByteArrayInputStream;
020import java.io.File;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028
029import com.fasterxml.jackson.core.JsonParser;
030import com.fasterxml.jackson.databind.JsonNode;
031import com.fasterxml.jackson.databind.ObjectMapper;
032import org.apache.logging.log4j.core.config.AbstractConfiguration;
033import org.apache.logging.log4j.core.config.Configuration;
034import org.apache.logging.log4j.core.config.ConfigurationSource;
035import org.apache.logging.log4j.core.config.ConfiguratonFileWatcher;
036import org.apache.logging.log4j.core.config.LoggerConfig;
037import org.apache.logging.log4j.core.config.Node;
038import org.apache.logging.log4j.core.config.Reconfigurable;
039import org.apache.logging.log4j.core.config.plugins.util.PluginType;
040import org.apache.logging.log4j.core.config.plugins.util.ResolverUtil;
041import org.apache.logging.log4j.core.config.status.StatusConfiguration;
042import org.apache.logging.log4j.core.util.FileWatcher;
043import org.apache.logging.log4j.core.util.Patterns;
044
045/**
046 * Creates a Node hierarchy from a JSON file.
047 */
048public class JsonConfiguration extends AbstractConfiguration implements Reconfigurable {
049
050    private static final String[] VERBOSE_CLASSES = new String[] { ResolverUtil.class.getName() };
051    private final List<Status> status = new ArrayList<>();
052    private JsonNode root;
053
054    public JsonConfiguration(final ConfigurationSource configSource) {
055        super(configSource);
056        final File configFile = configSource.getFile();
057        byte[] buffer;
058        try {
059            try (final InputStream configStream = configSource.getInputStream()) {
060                buffer = toByteArray(configStream);
061            }
062            final InputStream is = new ByteArrayInputStream(buffer);
063            root = getObjectMapper().readTree(is);
064            if (root.size() == 1) {
065                for (final JsonNode node : root) {
066                    root = node;
067                }
068            }
069            processAttributes(rootNode, root);
070            final StatusConfiguration statusConfig = new StatusConfiguration().withVerboseClasses(VERBOSE_CLASSES)
071                    .withStatus(getDefaultStatus());
072            for (final Map.Entry<String, String> entry : rootNode.getAttributes().entrySet()) {
073                final String key = entry.getKey();
074                final String value = getStrSubstitutor().replace(entry.getValue());
075                // TODO: this duplicates a lot of the XmlConfiguration constructor
076                if ("status".equalsIgnoreCase(key)) {
077                    statusConfig.withStatus(value);
078                } else if ("dest".equalsIgnoreCase(key)) {
079                    statusConfig.withDestination(value);
080                } else if ("shutdownHook".equalsIgnoreCase(key)) {
081                    isShutdownHookEnabled = !"disable".equalsIgnoreCase(value);
082                } else if ("verbose".equalsIgnoreCase(entry.getKey())) {
083                    statusConfig.withVerbosity(value);
084                } else if ("packages".equalsIgnoreCase(key)) {
085                    pluginPackages.addAll(Arrays.asList(value.split(Patterns.COMMA_SEPARATOR)));
086                } else if ("name".equalsIgnoreCase(key)) {
087                    setName(value);
088                } else if ("monitorInterval".equalsIgnoreCase(key)) {
089                    final int intervalSeconds = Integer.parseInt(value);
090                    if (intervalSeconds > 0) {
091                        getWatchManager().setIntervalSeconds(intervalSeconds);
092                        if (configFile != null) {
093                            FileWatcher watcher = new ConfiguratonFileWatcher(this, listeners);
094                            getWatchManager().watchFile(configFile, watcher);
095                        }
096                    }
097                } else if ("advertiser".equalsIgnoreCase(key)) {
098                    createAdvertiser(value, configSource, buffer, "application/json");
099                }
100            }
101            statusConfig.initialize();
102            if (getName() == null) {
103                setName(configSource.getLocation());
104            }
105        } catch (final Exception ex) {
106            LOGGER.error("Error parsing " + configSource.getLocation(), ex);
107        }
108    }
109
110    protected ObjectMapper getObjectMapper() {
111        return new ObjectMapper().configure(JsonParser.Feature.ALLOW_COMMENTS, true);
112    }
113
114    @Override
115    public void setup() {
116        final Iterator<Map.Entry<String, JsonNode>> iter = root.fields();
117        final List<Node> children = rootNode.getChildren();
118        while (iter.hasNext()) {
119            final Map.Entry<String, JsonNode> entry = iter.next();
120            final JsonNode n = entry.getValue();
121            if (n.isObject()) {
122                LOGGER.debug("Processing node for object {}", entry.getKey());
123                children.add(constructNode(entry.getKey(), rootNode, n));
124            } else if (n.isArray()) {
125                LOGGER.error("Arrays are not supported at the root configuration.");
126            }
127        }
128        LOGGER.debug("Completed parsing configuration");
129        if (status.size() > 0) {
130            for (final Status s : status) {
131                LOGGER.error("Error processing element " + s.name + ": " + s.errorType);
132            }
133        }
134    }
135
136    @Override
137    public Configuration reconfigure() {
138        try {
139            final ConfigurationSource source = getConfigurationSource().resetInputStream();
140            if (source == null) {
141                return null;
142            }
143            return new JsonConfiguration(source);
144        } catch (final IOException ex) {
145            LOGGER.error("Cannot locate file {}", getConfigurationSource(), ex);
146        }
147        return null;
148    }
149
150    private Node constructNode(final String name, final Node parent, final JsonNode jsonNode) {
151        final PluginType<?> type = pluginManager.getPluginType(name);
152        final Node node = new Node(parent, name, type);
153        processAttributes(node, jsonNode);
154        final Iterator<Map.Entry<String, JsonNode>> iter = jsonNode.fields();
155        final List<Node> children = node.getChildren();
156        while (iter.hasNext()) {
157            final Map.Entry<String, JsonNode> entry = iter.next();
158            final JsonNode n = entry.getValue();
159            if (n.isArray() || n.isObject()) {
160                if (type == null) {
161                    status.add(new Status(name, n, ErrorType.CLASS_NOT_FOUND));
162                }
163                if (n.isArray()) {
164                    LOGGER.debug("Processing node for array {}", entry.getKey());
165                    for (int i = 0; i < n.size(); ++i) {
166                        final String pluginType = getType(n.get(i), entry.getKey());
167                        final PluginType<?> entryType = pluginManager.getPluginType(pluginType);
168                        final Node item = new Node(node, entry.getKey(), entryType);
169                        processAttributes(item, n.get(i));
170                        if (pluginType.equals(entry.getKey())) {
171                            LOGGER.debug("Processing {}[{}]", entry.getKey(), i);
172                        } else {
173                            LOGGER.debug("Processing {} {}[{}]", pluginType, entry.getKey(), i);
174                        }
175                        final Iterator<Map.Entry<String, JsonNode>> itemIter = n.get(i).fields();
176                        final List<Node> itemChildren = item.getChildren();
177                        while (itemIter.hasNext()) {
178                            final Map.Entry<String, JsonNode> itemEntry = itemIter.next();
179                            if (itemEntry.getValue().isObject()) {
180                                LOGGER.debug("Processing node for object {}", itemEntry.getKey());
181                                itemChildren.add(constructNode(itemEntry.getKey(), item, itemEntry.getValue()));
182                            } else if (itemEntry.getValue().isArray()) {
183                                final JsonNode array = itemEntry.getValue();
184                                final String entryName = itemEntry.getKey();
185                                LOGGER.debug("Processing array for object {}", entryName);
186                                for (int j = 0; j < array.size(); ++j) {
187                                    itemChildren.add(constructNode(entryName, item, array.get(j)));
188                                }
189                            }
190
191                        }
192                        children.add(item);
193                    }
194                } else {
195                    LOGGER.debug("Processing node for object {}", entry.getKey());
196                    children.add(constructNode(entry.getKey(), node, n));
197                }
198            } else {
199                LOGGER.debug("Node {} is of type {}", entry.getKey(), n.getNodeType());
200            }
201        }
202
203        String t;
204        if (type == null) {
205            t = "null";
206        } else {
207            t = type.getElementName() + ':' + type.getPluginClass();
208        }
209
210        final String p = node.getParent() == null ? "null"
211                : node.getParent().getName() == null ? LoggerConfig.ROOT : node.getParent().getName();
212        LOGGER.debug("Returning {} with parent {} of type {}", node.getName(), p, t);
213        return node;
214    }
215
216    private String getType(final JsonNode node, final String name) {
217        final Iterator<Map.Entry<String, JsonNode>> iter = node.fields();
218        while (iter.hasNext()) {
219            final Map.Entry<String, JsonNode> entry = iter.next();
220            if (entry.getKey().equalsIgnoreCase("type")) {
221                final JsonNode n = entry.getValue();
222                if (n.isValueNode()) {
223                    return n.asText();
224                }
225            }
226        }
227        return name;
228    }
229
230    private void processAttributes(final Node parent, final JsonNode node) {
231        final Map<String, String> attrs = parent.getAttributes();
232        final Iterator<Map.Entry<String, JsonNode>> iter = node.fields();
233        while (iter.hasNext()) {
234            final Map.Entry<String, JsonNode> entry = iter.next();
235            if (!entry.getKey().equalsIgnoreCase("type")) {
236                final JsonNode n = entry.getValue();
237                if (n.isValueNode()) {
238                    attrs.put(entry.getKey(), n.asText());
239                }
240            }
241        }
242    }
243
244    @Override
245    public String toString() {
246        return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]";
247    }
248
249    /**
250     * The error that occurred.
251     */
252    private enum ErrorType {
253        CLASS_NOT_FOUND
254    }
255
256    /**
257     * Status for recording errors.
258     */
259    private static class Status {
260        private final JsonNode node;
261        private final String name;
262        private final ErrorType errorType;
263
264        public Status(final String name, final JsonNode node, final ErrorType errorType) {
265            this.name = name;
266            this.node = node;
267            this.errorType = errorType;
268        }
269
270        @Override
271        public String toString() {
272            return "Status [name=" + name + ", errorType=" + errorType + ", node=" + node + "]";
273        }
274    }
275}