001/*
002 * Copyright (C) 2019 Michael N. Lipp (http://www.mnl.de)
003 * 
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *        http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package de.mnl.osgi.jul2osgi;
018
019import de.mnl.osgi.coreutils.ServiceResolver;
020import de.mnl.osgi.jul2osgi.lib.LogManager;
021import de.mnl.osgi.jul2osgi.lib.LogManager.LogInfo;
022import de.mnl.osgi.jul2osgi.lib.LogRecordHandler;
023
024import java.lang.ref.WeakReference;
025import java.security.AccessController;
026import java.security.PrivilegedAction;
027import java.text.MessageFormat;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.Map;
031import java.util.MissingResourceException;
032import java.util.Optional;
033import java.util.Set;
034import java.util.WeakHashMap;
035import java.util.concurrent.ConcurrentHashMap;
036import java.util.logging.Level;
037import java.util.logging.LogRecord;
038
039import org.osgi.framework.Bundle;
040import org.osgi.framework.BundleContext;
041import org.osgi.framework.FrameworkUtil;
042import org.osgi.service.log.LogLevel;
043import org.osgi.service.log.LogService;
044import org.osgi.service.log.Logger;
045import org.osgi.service.log.admin.LoggerAdmin;
046import org.osgi.service.log.admin.LoggerContext;
047
048/**
049 */
050@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
051public class Forwarder extends ServiceResolver implements LogRecordHandler {
052
053    private String logPattern;
054    private boolean adaptOsgiLevel = true;
055    private final Map<Class<?>, WeakReference<Bundle>> bundles
056        = new ConcurrentHashMap<>();
057    private final Set<Bundle> adaptedBundles = Collections
058        .synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
059
060    @Override
061    @SuppressWarnings({ "PMD.SystemPrintln" })
062    protected void configure() {
063        final java.util.logging.LogManager logMgr
064            = java.util.logging.LogManager.getLogManager();
065        if (!(logMgr instanceof LogManager)) {
066            System.err.println("Configuration error: "
067                + "Bundle de.mnl.osgi.jul2osgi must be used with "
068                + "LogManager from de.mnl.osgi.jul2osgi.log.");
069            return;
070        }
071        logPattern = Optional.ofNullable(
072            context.getProperty("de.mnl.osgi.jul2osgi.logPattern"))
073            .orElse("{0}");
074        String adaptProperty = context.getProperty(
075            "de.mnl.osgi.jul2osgi.adaptOSGiLevel");
076        if (adaptProperty != null) {
077            adaptOsgiLevel = Boolean.parseBoolean(adaptProperty);
078        }
079
080        addDependency(LogService.class);
081        addDependency(LoggerAdmin.class);
082        setOnResolved(() -> {
083            if (adaptOsgiLevel) {
084                // Handle this bundle specially
085                adaptLogLevel(get(LoggerAdmin.class),
086                    context.getBundle());
087            }
088            ((LogManager) logMgr).setForwarder(this);
089        });
090        setOnDissolving(() -> {
091            ((LogManager) logMgr).setForwarder(this);
092        });
093    }
094
095    @Override
096    public void stop(BundleContext context) throws Exception {
097        java.util.logging.LogManager logMgr
098            = java.util.logging.LogManager.getLogManager();
099        if (logMgr instanceof LogManager) {
100            ((LogManager) logMgr).setForwarder(null);
101        }
102        super.stop(context);
103    }
104
105    private void adaptLogLevel(LoggerAdmin logAdmin, Bundle bundle) {
106        if (adaptedBundles.contains(bundle)) {
107            return;
108        }
109        LoggerContext ctx = logAdmin.getLoggerContext(
110            bundle.getSymbolicName() + "|" + bundle.getVersion());
111        @SuppressWarnings("PMD.UseConcurrentHashMap")
112        Map<String, LogLevel> logLevels = new HashMap<>();
113        logLevels.put(Logger.ROOT_LOGGER_NAME, LogLevel.TRACE);
114        ctx.setLogLevels(logLevels);
115        adaptedBundles.add(bundle);
116    }
117
118    @Override
119    public boolean process(LogInfo logInfo) {
120        LogService logSvc = get(LogService.class);
121        if (logSvc == null) {
122            return false;
123        }
124        doProcess(logInfo, logSvc);
125        return true;
126    }
127
128    private void doProcess(LogInfo logInfo, LogService service) {
129        final LogRecord record = logInfo.getLogRecord();
130        final String loggerName = Optional.ofNullable(record.getLoggerName())
131            .orElse(Logger.ROOT_LOGGER_NAME);
132        Logger logger = findBundle(logInfo.getCallingClass())
133            .map(b -> {
134                if (adaptOsgiLevel) {
135                    adaptLogLevel(get(LoggerAdmin.class), b);
136                }
137                return service.getLogger(b, loggerName, Logger.class);
138            }).orElse(service.getLogger(loggerName));
139
140        int julLevel = record.getLevel().intValue();
141        if (julLevel >= Level.SEVERE.intValue()) {
142            if (logger.isErrorEnabled()) {
143                logger.error("{}", formatMessage(logPattern, record),
144                    record.getThrown());
145            }
146        } else if (julLevel >= Level.WARNING.intValue()) {
147            if (logger.isWarnEnabled()) {
148                logger.warn("{}", formatMessage(logPattern, record),
149                    record.getThrown());
150            }
151        } else if (julLevel >= Level.INFO.intValue()) {
152            if (logger.isInfoEnabled()) {
153                logger.info("{}", formatMessage(logPattern, record),
154                    record.getThrown());
155            }
156        } else if (julLevel >= Level.FINE.intValue()) {
157            if (logger.isDebugEnabled()) {
158                logger.debug("{}", formatMessage(logPattern, record),
159                    record.getThrown());
160            }
161        } else if (logger.isTraceEnabled()) {
162            logger.trace("{}", formatMessage(logPattern, record),
163                record.getThrown());
164        }
165    }
166
167    private Optional<Bundle> findBundle(Class<?> callingClass) {
168        if (callingClass == null) {
169            return Optional.empty();
170        }
171        Bundle bundle = Optional.ofNullable(bundles.get(callingClass))
172            .map(WeakReference::get).orElse(null);
173        if (bundle != null) {
174            return Optional.of(bundle);
175        }
176        bundle = FrameworkUtil.getBundle(callingClass);
177        if (bundle != null) {
178            bundles.put(callingClass, new WeakReference<>(bundle)); // NOPMD
179            return Optional.of(bundle);
180        }
181        return Optional.empty();
182    }
183
184    private String formatMessage(String format, LogRecord record) {
185        String message = record.getMessage();
186        if (record.getResourceBundle() != null) {
187            try {
188                message = record.getResourceBundle().getString(message);
189            } catch (MissingResourceException e) { // NOPMD
190                // Leave message as it is
191            }
192        }
193        Object[] parameters = record.getParameters();
194        if (parameters != null && parameters.length > 0) {
195            message = MessageFormat.format(message, record.getParameters());
196        }
197        return MessageFormat.format(format, message, record.getMillis(),
198            record.getSequenceNumber(), record.getSourceClassName(),
199            record.getSourceMethodName(), record.getThreadID());
200    }
201
202    /**
203     * Process events that are delivered .
204     *
205     * @param logInfos the log infos
206     */
207    @SuppressWarnings({ "PMD.EmptyCatchBlock", "PMD.UseVarargs" })
208    public void processBuffered(LogInfo[] logInfos) {
209        String threadName = Thread.currentThread().getName();
210        try {
211            // Set thread name to indicate invalid context
212            try {
213                AccessController.doPrivileged(new PrivilegedAction<Void>() {
214                    @Override
215                    public Void run() {
216                        Thread.currentThread().setName("(log flusher)");
217                        return null;
218                    }
219                });
220            } catch (SecurityException e) {
221                // Ignored, was just a best effort.
222            }
223            // Now process
224            for (LogInfo info : logInfos) {
225                process(info);
226            }
227        } finally {
228            try {
229                AccessController.doPrivileged(new PrivilegedAction<Void>() {
230
231                    @Override
232                    public Void run() {
233                        Thread.currentThread().setName(threadName);
234                        return null; // NOPMD
235                    }
236                });
237            } catch (SecurityException e) {
238                // Ignored. If resetting doesn't work, setting hasn't worked
239                // neither
240            }
241        }
242    }
243
244}