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.lf4osgi.core;
018
019import java.util.Map;
020import java.util.Optional;
021import java.util.concurrent.ConcurrentHashMap;
022import java.util.function.Function;
023
024import org.osgi.framework.Bundle;
025import org.osgi.framework.BundleEvent;
026import org.osgi.framework.BundleListener;
027import org.osgi.framework.FrameworkUtil;
028
029/**
030 * A manager for groups of loggers associated with a bundle. A Logger 
031 * group for a given bundle is automatically discarded when the 
032 * associated bundle is uninstalled.
033 *
034 * @param <T> the logger group type
035 */
036public class LoggerCatalogue<T> {
037
038    private static final ContextHelper CTX_HLPR = new ContextHelper();
039
040    private boolean listenerInstalled;
041    private final Map<Bundle, T> groups = new ConcurrentHashMap<>();
042    private final Function<Bundle, T> groupSupplier;
043
044    /**
045     * Instantiates a new logger catalogue.
046     *
047     * @param groupSupplier the supplier for new logger groups
048     */
049    public LoggerCatalogue(Function<Bundle, T> groupSupplier) {
050        super();
051        this.groupSupplier = groupSupplier;
052        /*
053         * This may be invoked from from anywhere, even a static context.
054         * Therefore, it might not be possible to register the listener
055         * immediately.
056         */
057        checkListener();
058    }
059
060    private void checkListener() {
061        if (listenerInstalled) {
062            return;
063        }
064        Optional.ofNullable(FrameworkUtil.getBundle(groupSupplier.getClass()))
065            .map(Bundle::getBundleContext).ifPresent(ctx -> {
066                ctx.addBundleListener(new BundleListener() {
067                    @Override
068                    public void bundleChanged(BundleEvent event) {
069                        if (event.getType() == BundleEvent.UNINSTALLED) {
070                            groups.remove(event.getBundle());
071                        }
072                    }
073                });
074                listenerInstalled = true;
075                // This is unlikely to ever happen, but as this is delayed...
076                for (Bundle bdl : groups.keySet()) {
077                    if ((bdl.getState() & Bundle.UNINSTALLED) != 0) {
078                        groups.remove(bdl);
079                    }
080                }
081            });
082    }
083
084    /**
085     * Find the class that attempts to get a logger. This is done 
086     * by searching through the current call stack for the invocation 
087     * of the {@code getLogger} method of the class that provides 
088     * the loggers (the {@code providingClass}). The next frame in 
089     * the call stack then reveals the name of the class that 
090     * requests the logger.
091     *
092     * @param providingClass the providing class
093     * @return the optional
094     */
095    private static Optional<Class<?>>
096            findRequestingClass(String providingClass) {
097        StackTraceElement[] stackTrace = new Throwable().getStackTrace();
098        for (int i = 2; i < stackTrace.length; i++) {
099            StackTraceElement ste = stackTrace[i];
100            if (ste.getClassName().equals(providingClass)
101                && ste.getMethodName().equals("getLogger")) {
102                Class<?>[] classes = CTX_HLPR.getClassContext();
103                // getClassContext() adds one level, so the
104                // call of getLogger (in classes) should be at i+1.
105                i += 1;
106                // But... from the JavaDoc: "Some virtual machines may, under
107                // some circumstances, omit one or more stack frames
108                // from the stack trace." So let's make sure that we
109                // are really there.
110                while (!classes[i].getName().equals(providingClass)) {
111                    i += 1; // NOPMD
112                }
113                // Next one should now be the caller of getLogger. But
114                // in some libraries, getLogger calls "itself", e.g.
115                // getLogger(Class<?>) calls getLogger(String), proceed
116                // until we're out of this
117                while (classes[i].getName().equals(providingClass)) {
118                    i += 1; // NOPMD
119                }
120                return Optional.of(classes[i]);
121            }
122        }
123        return Optional.empty();
124    }
125
126    /**
127     * Find the bundle that contains the class that wants to get
128     * a logger, using the current call stack. 
129     * <P>
130     * The bundle is determined from the class that invoked 
131     * {@code getLogger}, which&mdash;in turn&mdash;is searched for in 
132     * the call stack as caller of the {@code getLogger} method of the 
133     * class that provides the loggers from the users point of view.
134     *
135     * @param providingClass the providing class
136     * @return the bundle
137     */
138    public static Optional<Bundle> findBundle(String providingClass) {
139        return findRequestingClass(providingClass)
140            .map(cls -> FrameworkUtil.getBundle(cls));
141    }
142
143    /**
144     * Returns the logger group for the given bundle.
145     *
146     * @param bundle the bundle
147     * @return the logger group
148     */
149    public T getLoggerGoup(Bundle bundle) {
150        checkListener();
151        return groups.computeIfAbsent(bundle, b -> groupSupplier.apply(b));
152    }
153
154    /**
155     * The Class ContextHelper.
156     */
157    private static class ContextHelper extends SecurityManager {
158        @Override
159        @SuppressWarnings("PMD.UselessOverridingMethod")
160        public Class<?>[] getClassContext() {
161            return super.getClassContext();
162        }
163    }
164}