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—in turn—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}