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;
023import java.lang.ref.WeakReference;
024import java.security.AccessController;
025import java.security.PrivilegedAction;
026import java.text.MessageFormat;
027import java.util.Collections;
028import java.util.HashMap;
029import java.util.Map;
030import java.util.MissingResourceException;
031import java.util.Optional;
032import java.util.Set;
033import java.util.WeakHashMap;
034import java.util.concurrent.ConcurrentHashMap;
035import java.util.logging.Level;
036import java.util.logging.LogRecord;
037import org.osgi.framework.Bundle;
038import org.osgi.framework.BundleContext;
039import org.osgi.framework.FrameworkUtil;
040import org.osgi.service.log.LogLevel;
041import org.osgi.service.log.LogService;
042import org.osgi.service.log.Logger;
043import org.osgi.service.log.admin.LoggerAdmin;
044import org.osgi.service.log.admin.LoggerContext;
045
046/**
047 */
048@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
049public class Forwarder extends ServiceResolver implements LogRecordHandler {
050
051    private String logPattern;
052    private boolean adaptOsgiLevel = true;
053    private final Map<Class<?>, WeakReference<Bundle>> bundles
054        = new ConcurrentHashMap<>();
055    private final Set<Bundle> adaptedBundles = Collections
056        .synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
057
058    @Override
059    @SuppressWarnings({ "PMD.SystemPrintln" })
060    protected void configure() {
061        final java.util.logging.LogManager logMgr
062            = java.util.logging.LogManager.getLogManager();
063        if (!(logMgr instanceof LogManager)) {
064            System.err.println("Configuration error: "
065                + "Bundle de.mnl.osgi.jul2osgi must be used with "
066                + "LogManager from de.mnl.osgi.jul2osgi.log.");
067            return;
068        }
069        logPattern = Optional.ofNullable(
070            context.getProperty("de.mnl.osgi.jul2osgi.logPattern"))
071            .orElse("{0}");
072        String adaptProperty = context.getProperty(
073            "de.mnl.osgi.jul2osgi.adaptOSGiLevel");
074        if (adaptProperty != null) {
075            adaptOsgiLevel = Boolean.parseBoolean(adaptProperty);
076        }
077
078        addDependency(LogService.class);
079        addDependency(LoggerAdmin.class);
080        setOnResolved(() -> {
081            if (adaptOsgiLevel) {
082                // Handle this bundle specially
083                adaptLogLevel(get(LoggerAdmin.class),
084                    context.getBundle());
085            }
086            ((LogManager) logMgr).setForwarder(this);
087        });
088        setOnDissolving(() -> {
089            ((LogManager) logMgr).setForwarder(null);
090            adaptedBundles.clear();
091        });
092    }
093
094    @Override
095    public void stop(BundleContext context) throws Exception {
096        java.util.logging.LogManager logMgr
097            = java.util.logging.LogManager.getLogManager();
098        if (logMgr instanceof LogManager) {
099            ((LogManager) logMgr).setForwarder(null);
100        }
101        super.stop(context);
102    }
103
104    private void adaptLogLevel(LoggerAdmin logAdmin, Bundle bundle) {
105        if (adaptedBundles.contains(bundle)) {
106            return;
107        }
108        LoggerContext ctx = logAdmin.getLoggerContext(
109            bundle.getSymbolicName() + "|" + bundle.getVersion());
110        @SuppressWarnings("PMD.UseConcurrentHashMap")
111        Map<String, LogLevel> logLevels = new HashMap<>();
112        logLevels.put(Logger.ROOT_LOGGER_NAME, LogLevel.TRACE);
113        ctx.setLogLevels(logLevels);
114        adaptedBundles.add(bundle);
115    }
116
117    @Override
118    public boolean process(LogInfo logInfo) {
119        LogService logSvc = get(LogService.class);
120        if (logSvc == null) {
121            return false;
122        }
123        doProcess(logInfo, logSvc);
124        return true;
125    }
126
127    @SuppressWarnings("PMD.CognitiveComplexity")
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        "PMD.AvoidInstantiatingObjectsInLoops" })
209    public void processBuffered(LogInfo[] logInfos) {
210        String threadName = Thread.currentThread().getName();
211        for (LogInfo info : logInfos) {
212            try {
213                // Set thread name to indicate invalid context
214                try {
215                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
216                        @Override
217                        public Void run() {
218                            Thread.currentThread()
219                                .setName(info.getThreadName() + " [recorded]");
220                            return null;
221                        }
222                    });
223                } catch (SecurityException e) {
224                    // Ignored, was just a best effort.
225                }
226                // Now process
227                process(info);
228            } finally {
229                try {
230                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
231
232                        @Override
233                        public Void run() {
234                            Thread.currentThread().setName(threadName);
235                            return null; // NOPMD
236                        }
237                    });
238                } catch (SecurityException e) {
239                    // Ignored. If resetting doesn't work, setting hasn't worked
240                    // neither
241                }
242            }
243        }
244    }
245
246}