/** * Copyright (C) 2012-2017 52°North Initiative for Geospatial Open Source * Software GmbH * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 as published * by the Free Software Foundation. * * If the program is linked with libraries which are licensed under one of * the following licenses, the combination of the program with the linked * library is not considered a "derivative work" of the program: * * - Apache License, version 2.0 * - Apache Software License, version 1.0 * - GNU Lesser General Public License, version 3 * - Mozilla Public License, versions 1.0, 1.1 and 2.0 * - Common Development and Distribution License (CDDL), version 1.0 * * Therefore the distribution of the program linked with libraries licensed * under the aforementioned licenses, is permitted by the copyright holders * if the distribution is compliant with both the GNU General Public * License version 2 and the aforementioned licenses. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. */ package org.n52.sos.web; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.n52.sos.exception.ConfigurationException; import org.n52.sos.ogc.ows.OwsExceptionReport; import org.n52.sos.service.AbstractLoggingConfigurator; import org.n52.sos.util.FileIOHelper; import org.slf4j.Logger; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * @since 4.0.0 * */ public class LogBackLoggingConfigurator extends AbstractLoggingConfigurator { private static final String CONFIGURATION_FILE_NAME = "/logback.xml"; private static final String AN_LEVEL = "level"; private static final String AN_NAME = "name"; private static final String AN_REF = "ref"; private static final String AN_VALUE = "value"; private static final String EN_ROLLING_POLICY = "rollingPolicy"; private static final String EN_MAX_HISTORY = "maxHistory"; private static final String EN_APPENDER = "appender"; private static final String EN_APPENDER_REF = "appender-ref"; private static final String EN_ROOT = "root"; private static final String EN_LOGGER = "logger"; private static final String EN_FILE = "file"; private static final String EN_PROPERTY = "property"; private static final String EN_MAX_FILE_SIZE = "maxFileSize"; private static final String EN_TIME_BASED_FILE_NAME_AND_TRIGGERING_POLICY = "timeBasedFileNamingAndTriggeringPolicy"; private static final String NOT_FOUND_ERROR_MESSAGE = "Can't find Logback configuration file."; private static final String UNPARSABLE_ERROR_MESSAGE = "Can't parse configuration file."; private static final String UNWRITABLE_ERROR_MESSAGE = "Can't write configuration file."; private static final String LOG_FILE_NOT_FOUND_ERROR_MESSAGE = "Log file could not be found"; private static final int WRITE_DELAY = 4000; private static final Pattern PROPERTY_MATCHER = Pattern.compile("\\$\\{([^}]+)\\}"); private static final ReadWriteLock LOCK = new ReentrantReadWriteLock(); private Document cache = null; private File configuration = null; private DelayedWriteThread delayedWriteThread = null; private class DelayedWriteThread extends Thread { private Document doc; private boolean canceled = false; DelayedWriteThread(Document doc) { this.doc = doc; } @Override public void run() { try { Thread.sleep(WRITE_DELAY); synchronized (this) { if (!canceled) { write(); } } } catch (InterruptedException e) { } } void cancel() { synchronized (this) { canceled = true; } } void write() { LOCK.writeLock().lock(); LOG.debug("Writing LogBack configuration file!"); try { FileOutputStream out = null; try { out = new FileOutputStream(configuration); Transformer trans = TransformerFactory.newInstance().newTransformer(); trans.setOutputProperty(OutputKeys.INDENT, "yes"); OutputStreamWriter writer = new OutputStreamWriter(out); trans.transform(new DOMSource(doc), new StreamResult(writer)); } catch (TransformerException ex) { LOG.error(UNWRITABLE_ERROR_MESSAGE, ex); } catch (IOException ex) { LOG.error(UNWRITABLE_ERROR_MESSAGE, ex); } finally { if (out != null) { try { out.close(); } catch (IOException e) { LOG.error(UNWRITABLE_ERROR_MESSAGE, e); } } } } finally { LOCK.writeLock().unlock(); } } } public LogBackLoggingConfigurator() throws ConfigurationException { this(CONFIGURATION_FILE_NAME); } public LogBackLoggingConfigurator(String filename) throws ConfigurationException { this(getFile(filename)); } private static File getFile(String name) throws ConfigurationException { File f = new File(name); if (f.exists()) { return f; } URL url = LogBackLoggingConfigurator.class.getResource(name); try { return new File(url.toURI()); } catch (Exception ex) { LOG.error(NOT_FOUND_ERROR_MESSAGE, ex); throw new ConfigurationException(NOT_FOUND_ERROR_MESSAGE, ex); } } public LogBackLoggingConfigurator(File file) throws ConfigurationException { configuration = file; if (configuration == null || !configuration.exists()) { LOG.error(NOT_FOUND_ERROR_MESSAGE); throw new ConfigurationException(NOT_FOUND_ERROR_MESSAGE); } LOG.info("Using Logback Config File: {}", configuration.getAbsolutePath()); } private Document read() throws ConfigurationException { LOCK.readLock().lock(); try { try { if (cache == null) { cache = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(configuration); } return cache; } catch (ParserConfigurationException ex) { LOG.error(UNPARSABLE_ERROR_MESSAGE, ex); throw new ConfigurationException(UNPARSABLE_ERROR_MESSAGE, ex); } catch (SAXException ex) { LOG.error(UNPARSABLE_ERROR_MESSAGE, ex); throw new ConfigurationException(UNPARSABLE_ERROR_MESSAGE, ex); } catch (IOException ex) { LOG.error(UNPARSABLE_ERROR_MESSAGE, ex); throw new ConfigurationException(UNPARSABLE_ERROR_MESSAGE, ex); } } finally { LOCK.readLock().unlock(); } } private void write() { LOCK.writeLock().lock(); try { /* delay the actual writing to aggregate changes to one IO task */ if (this.delayedWriteThread != null) { this.delayedWriteThread.cancel(); this.delayedWriteThread.interrupt(); } this.delayedWriteThread = new DelayedWriteThread(this.cache); this.delayedWriteThread.start(); } finally { LOCK.writeLock().unlock(); } } @Override public boolean setMaxHistory(int days) { LOCK.writeLock().lock(); try { Document doc = read(); List<Element> appender = getChildren(doc.getDocumentElement(), EN_APPENDER); for (Element a : appender) { if (getAttribute(a, AN_NAME).getValue().equals(Appender.FILE.getName())) { Element rollingPolicy = getSingleChildren(a, EN_ROLLING_POLICY); Element maxHistory = getSingleChildren(rollingPolicy, EN_MAX_HISTORY); int before = -1; try { before = Integer.parseInt(maxHistory.getTextContent()); } catch (NumberFormatException e) { } if (before != days) { LOG.debug("Setting max logging history to {} days.", days); maxHistory.setTextContent(String.valueOf(days)); } } } write(); } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); return false; } finally { LOCK.writeLock().unlock(); } return true; } @Override public Set<Appender> getEnabledAppender() { LOCK.readLock().lock(); Set<Appender> appender = new HashSet<Appender>(Appender.values().length); try { List<Element> refs = getChildren(getRoot(read().getDocumentElement()), EN_APPENDER_REF); for (Element ref : refs) { appender.add(Appender.byName(getAttribute(ref, AN_REF).getValue())); } } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); return Collections.emptySet(); } finally { LOCK.readLock().unlock(); } return appender; } @Override public boolean isEnabled(Appender a) { LOCK.readLock().lock(); try { return getEnabledAppender().contains(a); } finally { LOCK.readLock().unlock(); } } @Override public boolean enableAppender(Appender a, boolean enable) { LOCK.writeLock().lock(); try { Document doc = read(); Element root = getRoot(doc.getDocumentElement()); Element refNode = null; List<Element> refs = getChildren(root, EN_APPENDER_REF); for (Element ref : refs) { if (getAttribute(ref, AN_REF).getValue().equals(a.getName())) { refNode = ref; break; } } if (enable && refNode == null) { LOG.debug("Enabling {} logging appender", a.getName()); refNode = doc.createElement(EN_APPENDER_REF); refNode.setAttribute(AN_REF, a.getName()); root.appendChild(refNode); write(); } else if (!enable && refNode != null) { LOG.debug("Disabling {} logging appender", a.getName()); root.removeChild(refNode); write(); } return true; } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); return false; } finally { LOCK.writeLock().unlock(); } } private Element getSingleChildren(Node parent, String name) throws ConfigurationException { NodeList nl = parent.getChildNodes(); for (int i = 0; i < nl.getLength(); ++i) { Node n = nl.item(i); if (name.equals(n.getNodeName())) { return (Element) n; } } throw new ConfigurationException("<" + name + "> not found!"); } private Element getRoot(Node configuration) throws ConfigurationException { return getSingleChildren(configuration, EN_ROOT); } private Attr getAttribute(Node x, String name) throws ConfigurationException { NamedNodeMap attributes = x.getAttributes(); Attr a = (Attr) attributes.getNamedItem(name); if (a != null) { return a; } throw new ConfigurationException("Missing attribute: " + name); } @Override public boolean setRootLogLevel(Level level) { LOCK.writeLock().lock(); try { try { Document doc = read(); Element root = getRoot(doc.getDocumentElement()); String currentLevel = getAttribute(root, AN_LEVEL).getValue(); if (Level.valueOf(currentLevel) == level) { return true; } LOG.debug("Setting root logging level to {}", level); root.setAttribute(AN_LEVEL, level.toString()); write(); } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); return false; } return true; } finally { LOCK.writeLock().unlock(); } } private List<Element> getChildren(Element parent, String name) { NodeList nl = parent.getChildNodes(); ArrayList<Element> childs = new ArrayList<Element>(nl.getLength()); for (int i = 0; i < nl.getLength(); ++i) { if (nl.item(i).getNodeType() == Node.ELEMENT_NODE && nl.item(i).getNodeName().equals(name)) { childs.add((Element) nl.item(i)); } } return childs; } @Override public boolean setLoggerLevel(String id, Level level) { LOCK.writeLock().lock(); try { if (id.equals(Logger.ROOT_LOGGER_NAME)) { return setRootLogLevel(level); } Document doc = read(); Element conf = doc.getDocumentElement(); Element l = null; List<Element> loggers = getChildren(conf, EN_LOGGER); for (Element logger : loggers) { if (getAttribute(logger, AN_NAME).getValue().equals(id)) { l = logger; } } if (l == null) { LOG.debug("Setting logging level of {} to {}.", id, level); l = doc.createElement(EN_LOGGER); l.setAttribute(AN_NAME, id); l.setAttribute(AN_LEVEL, level.name()); conf.appendChild(l); write(); } else { String oldLevel = l.getAttribute(AN_LEVEL); if (!oldLevel.equals(level.name())) { LOG.debug("Setting logging level of {} to {}.", id, level); l.setAttribute(AN_LEVEL, level.name()); write(); } } return true; } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); return false; } finally { LOCK.writeLock().unlock(); } } @Override public boolean setLoggerLevel(Map<String, Level> levels) { LOCK.writeLock().lock(); try { Document doc = read(); Element conf = doc.getDocumentElement(); List<Element> loggers = getChildren(conf, EN_LOGGER); Map<String, Element> currentLoggers = new HashMap<String, Element>(loggers.size()); for (Element logger : loggers) { currentLoggers.put(getAttribute(logger, AN_NAME).getValue(), logger); } boolean write = false; /* remove obsolete loggers */ for (String logger : currentLoggers.keySet()) { if (levels.get(logger) == null) { LOG.debug("Removing logger {}", logger); conf.removeChild(currentLoggers.get(logger)); write = true; } } for (String logger : levels.keySet()) { if (logger.equals(Logger.ROOT_LOGGER_NAME)) { setRootLogLevel(levels.get(logger)); } else { Element l = currentLoggers.get(logger); if (l == null) { LOG.debug("Setting logging level of {} to {}.", logger, levels.get(logger)); l = doc.createElement(EN_LOGGER); l.setAttribute(AN_NAME, logger); l.setAttribute(AN_LEVEL, levels.get(logger).name()); conf.appendChild(l); write = true; } else { String oldLevel = l.getAttribute(AN_LEVEL); if (!oldLevel.equals(levels.get(logger).name())) { LOG.debug("Setting logging level of {} to {}.", logger, levels.get(logger)); l.setAttribute(AN_LEVEL, levels.get(logger).name()); write = true; } } } } if (write) { write(); } return true; } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); return false; } finally { LOCK.writeLock().unlock(); } } @Override public Level getRootLogLevel() { LOCK.readLock().lock(); try { Level level = null; try { Document doc = read(); Element root = getRoot(doc.getDocumentElement()); String currentLevel = getAttribute(root, AN_LEVEL).getValue(); level = Level.valueOf(currentLevel); } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); } return level; } finally { LOCK.readLock().unlock(); } } @Override public Map<String, Level> getLoggerLevels() { LOCK.readLock().lock(); try { Map<String, Level> levels = new HashMap<String, Level>(); try { List<Element> loggers = getChildren(read().getDocumentElement(), EN_LOGGER); for (Element logger : loggers) { levels.put(getAttribute(logger, AN_NAME).getValue(), Level.valueOf(getAttribute(logger, AN_LEVEL).getValue())); } } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); } return levels; } finally { LOCK.readLock().unlock(); } } @Override public Level getLoggerLevel(String id) { LOCK.readLock().lock(); try { if (id.equals(Logger.ROOT_LOGGER_NAME)) { return getRootLogLevel(); } return getLoggerLevels().get(id); } finally { LOCK.readLock().unlock(); } } @Override public int getMaxHistory() { LOCK.readLock().lock(); try { int max = -1; try { Document doc = read(); List<Element> appender = getChildren(doc.getDocumentElement(), EN_APPENDER); for (Element a : appender) { if (getAttribute(a, AN_NAME).getValue().equals(Appender.FILE.getName())) { try { max = Integer.parseInt(getSingleChildren(getSingleChildren(a, EN_ROLLING_POLICY), EN_MAX_HISTORY).getTextContent()); } catch (NumberFormatException e) { } } } } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); } return max; } finally { LOCK.readLock().unlock(); } } @Override public String getMaxFileSize() { LOCK.readLock().lock(); try { String maxFileSize = null; try { Document doc = read(); List<Element> appender = getChildren(doc.getDocumentElement(), EN_APPENDER); for (Element a : appender) { if (getAttribute(a, AN_NAME).getValue().equals(Appender.FILE.getName())) { maxFileSize = getSingleChildren( getSingleChildren(getSingleChildren(a, EN_ROLLING_POLICY), EN_TIME_BASED_FILE_NAME_AND_TRIGGERING_POLICY), EN_MAX_FILE_SIZE) .getTextContent(); } } } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); } return maxFileSize; } finally { LOCK.readLock().unlock(); } } @Override public boolean setMaxFileSize(String size) { if (size == null) { return false; } LOCK.writeLock().lock(); try { Document doc = read(); List<Element> appender = getChildren(doc.getDocumentElement(), EN_APPENDER); for (Element a : appender) { if (getAttribute(a, AN_NAME).getValue().equals(Appender.FILE.getName())) { Element maxFileSize = getSingleChildren( getSingleChildren(getSingleChildren(a, EN_ROLLING_POLICY), EN_TIME_BASED_FILE_NAME_AND_TRIGGERING_POLICY), EN_MAX_FILE_SIZE); String before = maxFileSize.getTextContent().trim(); if (!before.equals(size.trim())) { LOG.debug("Setting max logging file size to {}.", size); maxFileSize.setTextContent(size.trim()); } } } write(); } catch (ConfigurationException e) { LOG.error(UNPARSABLE_ERROR_MESSAGE, e); return false; } finally { LOCK.writeLock().unlock(); } return true; } @Override public List<String> getLastLogEntries(int maxSize) { File f = getLogFile1(); if (f != null) { try { return FileIOHelper.tail(f, maxSize); } catch (IOException ex) { LOG.error("Could not read log file", ex); } } return Collections.emptyList(); } private File getLogFile1() { String file = null; LOCK.readLock().lock(); try { Element doc = read().getDocumentElement(); for (Element a : getChildren(doc, EN_APPENDER)) { if (getAttribute(a, AN_NAME).getValue().equals(Appender.FILE.getName())) { file = getSingleChildren(a, EN_FILE).getTextContent(); } } Map<String, String> properties = new HashMap<String, String>(); for (Element p : getChildren(doc, EN_PROPERTY)) { properties.put(getAttribute(p, AN_NAME).getValue(), getAttribute(p, AN_VALUE).getValue()); } if (file == null) { LOG.error(LOG_FILE_NOT_FOUND_ERROR_MESSAGE); return null; } Matcher matcher = PROPERTY_MATCHER.matcher(file); while (matcher.find()) { String key = matcher.group(1); String value = properties.get(key); if (value == null) { value = System.getProperty(key, null); } if (value == null) { LOG.error("Could not replace property {} in file name string {}", key, file); return null; } file = file.replace(matcher.group(), value); matcher = PROPERTY_MATCHER.matcher(file); } LOG.debug("Logfile: {}", file); File f = new File(file); if (!f.exists()) { LOG.error("Can not find log file {}", f.getAbsolutePath()); return null; } return f; } catch (ConfigurationException ex) { LOG.error(UNPARSABLE_ERROR_MESSAGE, ex); return null; } finally { LOCK.readLock().unlock(); } } @Override public InputStream getLogFile() { File f = getLogFile1(); if (f != null) { try { return FileIOHelper.loadInputStreamFromFile(f); } catch (OwsExceptionReport ex) { LOG.error("Could not read log file", ex); } } return null; } }