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