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(null);
092            adaptedBundles.clear();
093        });
094    }
095
096    @Override
097    public void stop(BundleContext context) throws Exception {
098        java.util.logging.LogManager logMgr
099            = java.util.logging.LogManager.getLogManager();
100        if (logMgr instanceof LogManager) {
101            ((LogManager) logMgr).setForwarder(null);
102        }
103        super.stop(context);
104    }
105
106    private void adaptLogLevel(LoggerAdmin logAdmin, Bundle bundle) {
107        if (adaptedBundles.contains(bundle)) {
108            return;
109        }
110        LoggerContext ctx = logAdmin.getLoggerContext(
111            bundle.getSymbolicName() + "|" + bundle.getVersion());
112        @SuppressWarnings("PMD.UseConcurrentHashMap")
113        Map<String, LogLevel> logLevels = new HashMap<>();
114        logLevels.put(Logger.ROOT_LOGGER_NAME, LogLevel.TRACE);
115        ctx.setLogLevels(logLevels);
116        adaptedBundles.add(bundle);
117    }
118
119    @Override
120    public boolean process(LogInfo logInfo) {
121        LogService logSvc = get(LogService.class);
122        if (logSvc == null) {
123            return false;
124        }
125        doProcess(logInfo, logSvc);
126        return true;
127    }
128
129    private void doProcess(LogInfo logInfo, LogService service) {
130        final LogRecord record = logInfo.getLogRecord();
131        final String loggerName = Optional.ofNullable(record.getLoggerName())
132            .orElse(Logger.ROOT_LOGGER_NAME);
133        Logger logger = findBundle(logInfo.getCallingClass())
134            .map(b -> {
135                if (adaptOsgiLevel) {
136                    adaptLogLevel(get(LoggerAdmin.class), b);
137                }
138                return service.getLogger(b, loggerName, Logger.class);
139            }).orElse(service.getLogger(loggerName));
140
141        int julLevel = record.getLevel().intValue();
142        if (julLevel >= Level.SEVERE.intValue()) {
143            if (logger.isErrorEnabled()) {
144                logger.error("{}", formatMessage(logPattern, record),
145                    record.getThrown());
146            }
147        } else if (julLevel >= Level.WARNING.intValue()) {
148            if (logger.isWarnEnabled()) {
149                logger.warn("{}", formatMessage(logPattern, record),
150                    record.getThrown());
151            }
152        } else if (julLevel >= Level.INFO.intValue()) {
153            if (logger.isInfoEnabled()) {
154                logger.info("{}", formatMessage(logPattern, record),
155                    record.getThrown());
156            }
157        } else if (julLevel >= Level.FINE.intValue()) {
158            if (logger.isDebugEnabled()) {
159                logger.debug("{}", formatMessage(logPattern, record),
160                    record.getThrown());
161            }
162        } else if (logger.isTraceEnabled()) {
163            logger.trace("{}", formatMessage(logPattern, record),
164                record.getThrown());
165        }
166    }
167
168    private Optional<Bundle> findBundle(Class<?> callingClass) {
169        if (callingClass == null) {
170            return Optional.empty();
171        }
172        Bundle bundle = Optional.ofNullable(bundles.get(callingClass))
173            .map(WeakReference::get).orElse(null);
174        if (bundle != null) {
175            return Optional.of(bundle);
176        }
177        bundle = FrameworkUtil.getBundle(callingClass);
178        if (bundle != null) {
179            bundles.put(callingClass, new WeakReference<>(bundle)); // NOPMD
180            return Optional.of(bundle);
181        }
182        return Optional.empty();
183    }
184
185    private String formatMessage(String format, LogRecord record) {
186        String message = record.getMessage();
187        if (record.getResourceBundle() != null) {
188            try {
189                message = record.getResourceBundle().getString(message);
190            } catch (MissingResourceException e) { // NOPMD
191                // Leave message as it is
192            }
193        }
194        Object[] parameters = record.getParameters();
195        if (parameters != null && parameters.length > 0) {
196            message = MessageFormat.format(message, record.getParameters());
197        }
198        return MessageFormat.format(format, message, record.getMillis(),
199            record.getSequenceNumber(), record.getSourceClassName(),
200            record.getSourceMethodName(), record.getThreadID());
201    }
202
203    /**
204     * Process events that are delivered .
205     *
206     * @param logInfos the log infos
207     */
208    @SuppressWarnings({ "PMD.EmptyCatchBlock", "PMD.UseVarargs",
209        "PMD.AvoidInstantiatingObjectsInLoops" })
210    public void processBuffered(LogInfo[] logInfos) {
211        String threadName = Thread.currentThread().getName();
212        for (LogInfo info : logInfos) {
213            try {
214                // Set thread name to indicate invalid context
215                try {
216                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
217                        @Override
218                        public Void run() {
219                            Thread.currentThread()
220                                .setName(info.getThreadName() + " [recorded]");
221                            return null;
222                        }
223                    });
224                } catch (SecurityException e) {
225                    // Ignored, was just a best effort.
226                }
227                // Now process
228                process(info);
229            } finally {
230                try {
231                    AccessController.doPrivileged(new PrivilegedAction<Void>() {
232
233                        @Override
234                        public Void run() {
235                            Thread.currentThread().setName(threadName);
236                            return null; // NOPMD
237                        }
238                    });
239                } catch (SecurityException e) {
240                    // Ignored. If resetting doesn't work, setting hasn't worked
241                    // neither
242                }
243            }
244        }
245    }
246
247}