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