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.osgi2jul;
018
019import de.mnl.osgi.coreutils.ServiceResolver;
020import java.lang.reflect.InvocationTargetException;
021import java.security.AccessController;
022import java.security.PrivilegedActionException;
023import java.security.PrivilegedExceptionAction;
024import java.text.MessageFormat;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.List;
029import java.util.Optional;
030import java.util.concurrent.CountDownLatch;
031import java.util.logging.Handler;
032import java.util.logging.Level;
033import java.util.logging.LogRecord;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036import org.osgi.framework.Bundle;
037import org.osgi.framework.BundleContext;
038import org.osgi.service.log.LogEntry;
039import org.osgi.service.log.LogReaderService;
040
041/**
042 * This class provides the activator for this service. It registers
043 * (respectively unregisters) the {@link LogWriter} as LogListener 
044 * for for all log reader services and forwards any already existing 
045 * log entries to it. 
046 */
047@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
048public class ForwardingManager extends ServiceResolver {
049
050    private static final Pattern HANDLER_DEF = Pattern.compile(
051        "(?:(?<bundle>[^:]+):)?(?<class>[^\\[]+)(?:\\[(?<id>\\d+)\\])?");
052
053    /** This tracker holds all log reader services. */
054    private final List<HandlerConfig> handlers = new ArrayList<>();
055    private LogReaderService subscribedService;
056    private LogWriter registeredListener;
057
058    /**
059     * Open the log service tracker. The tracker is customized to attach a 
060     * {@link LogWriter} to all registered log reader services (and detach 
061     * it on un-registration, of course). Already existing log entries 
062     * are forwarded to the {@link LogWriter} as well. No provisions have been
063     * taken to avoid the duplicate output that can occur if a message
064     * is logged between registering the {@link LogWriter} and forwarding
065     * stored log entries.
066     */
067    @Override
068    public void configure() {
069        createHandlers(context);
070        addDependency(LogReaderService.class);
071    }
072
073    /*
074     * (non-Javadoc)
075     * 
076     * @see de.mnl.osgi.coreutils.ServiceResolver#onResolved()
077     */
078    @Override
079    protected void onResolved() {
080        subscribeTo(get(LogReaderService.class));
081    }
082
083    @Override
084    protected void onRebound(String dependency) {
085        if (LogReaderService.class.getName().equals(dependency)) {
086            subscribeTo(get(LogReaderService.class));
087        }
088    }
089
090    @Override
091    protected void onDissolving() {
092        if (subscribedService != null && registeredListener != null) {
093            subscribedService.removeLogListener(registeredListener);
094        }
095        subscribedService = null;
096    }
097
098    private void subscribeTo(LogReaderService logReaderService) {
099        if (logReaderService.equals(subscribedService)) {
100            return;
101        }
102        if (subscribedService != null && registeredListener != null) {
103            subscribedService.removeLogListener(registeredListener);
104        }
105        subscribedService = logReaderService;
106        CountDownLatch enabled = new CountDownLatch(1);
107        registeredListener = new LogWriter(this, enabled);
108        subscribedService.addLogListener(registeredListener);
109        List<LogEntry> entries = Collections.list(subscribedService.getLog());
110        Collections.reverse(entries);
111        LogWriter historyWriter = new LogWriter(this, new CountDownLatch(0));
112        for (LogEntry entry : entries) {
113            historyWriter.logged(entry);
114        }
115        enabled.countDown();
116    }
117
118    @SuppressWarnings({ "PMD.SystemPrintln", "PMD.DataflowAnomalyAnalysis" })
119    private void createHandlers(BundleContext context) {
120        String handlerClasses = Optional.ofNullable(
121            context.getProperty(
122                ForwardingManager.class.getPackage().getName() + ".handlers"))
123            .orElse("java.util.logging.ConsoleHandler");
124        Arrays.stream(handlerClasses.split(",")).map(String::trim)
125            .forEach(name -> {
126                Matcher parts = HANDLER_DEF.matcher(name);
127                if (!parts.matches()) {
128                    System.err.println("Handler definition \""
129                        + name + "\" is invalid.");
130                    return;
131                }
132                Handler handler;
133                try {
134                    if (parts.group("bundle") == null) {
135                        // Only class name
136                        handler = handlerFromClassName(parts.group("class"));
137                    } else {
138                        handler = handlerFromBundledClass(context,
139                            parts.group("bundle"), parts.group("class"));
140                    }
141                } catch (Exception e) { // NOPMD
142                    System.err.println("Can't load or configure log handler \""
143                        + name + "\": " + e.getMessage());
144                    return;
145                }
146                handlers.add(createHandlerConfig(context, parts, handler));
147            });
148    }
149
150    @SuppressWarnings("PMD.AvoidUncheckedExceptionsInSignatures")
151    private Handler handlerFromBundledClass(BundleContext context,
152            String bundleName, String className)
153            throws InstantiationException, IllegalAccessException,
154            ClassNotFoundException, IllegalArgumentException,
155            InvocationTargetException, NoSuchMethodException,
156            SecurityException {
157        for (Bundle bundle : context.getBundles()) {
158            if (bundle.getSymbolicName().equals(bundleName)) {
159                return (Handler) bundle.loadClass(className)
160                    .getDeclaredConstructor().newInstance();
161            }
162        }
163        throw new ClassNotFoundException("Class " + className + " not found "
164            + "in bundle " + bundleName);
165    }
166
167    private Handler handlerFromClassName(String name)
168            throws PrivilegedActionException {
169        Handler handler;
170        handler = AccessController
171            .doPrivileged(new PrivilegedExceptionAction<Handler>() {
172                @Override
173                public Handler run() throws Exception {
174                    Class<?> hdlrCls;
175                    hdlrCls = ClassLoader.getSystemClassLoader()
176                        .loadClass(name);
177                    return (Handler) hdlrCls.getDeclaredConstructor()
178                        .newInstance();
179                }
180            });
181        return handler;
182    }
183
184    @SuppressWarnings("PMD.SystemPrintln")
185    private HandlerConfig createHandlerConfig(BundleContext context,
186            Matcher parts, Handler handler) {
187        String levelName = null;
188        MessageFormat outputFormat = null;
189        if (parts.group("id") != null) {
190            String handlerPrefix
191                = ForwardingManager.class.getPackage().getName()
192                    + ".handler" + (parts.group("id") == null ? ""
193                        : "." + parts.group("id"));
194            levelName = context.getProperty(handlerPrefix + ".level");
195            String format = context.getProperty(handlerPrefix + ".format");
196            if (format != null) {
197                try {
198                    outputFormat = new MessageFormat(format);
199                } catch (IllegalArgumentException e) {
200                    System.err.println("Illegal format: \"" + format + "\"");
201                }
202            }
203        }
204        if (levelName != null) {
205            Level level = Level.parse(levelName);
206            handler.setLevel(level);
207        }
208        return new HandlerConfig(handler, outputFormat);
209    }
210
211    @Override
212    public void stop(BundleContext context) throws Exception {
213        handlers.clear();
214        super.stop(context);
215    }
216
217    /**
218     * Send the record to all handlers. 
219     *
220     * @param entry the original OSGi log entry
221     * @param record the prepared JUL record
222     */
223    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
224    public void publish(LogEntry entry, LogRecord record) {
225        Object[] xtraArgs = null;
226        for (HandlerConfig cfg : handlers) {
227            if (cfg.getOutputFormat() == null) {
228                cfg.getHandler().publish(record);
229                continue;
230            }
231            String oldMessage = record.getMessage();
232            try {
233                if (xtraArgs == null) {
234                    xtraArgs = new Object[] {
235                        oldMessage,
236                        entry.getBundle().getSymbolicName(),
237                        entry.getBundle().getHeaders().get("Bundle-Name"),
238                        entry.getBundle().getVersion().toString(),
239                        entry.getThreadInfo() };
240                }
241                record.setMessage(cfg.getOutputFormat().format(xtraArgs,
242                    new StringBuffer(), null).toString());
243                cfg.getHandler().publish(record);
244            } finally {
245                record.setMessage(oldMessage);
246            }
247        }
248    }
249
250}