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