/*
* Copyright (C) 2014 GG-Net GmbH - Oliver Günther
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.ggnet.saft.core;
import java.awt.*;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import javax.ejb.*;
import javax.naming.*;
import javax.swing.JOptionPane;
import org.openide.util.Lookup;
import org.slf4j.*;
import static java.awt.TrayIcon.MessageType.WARNING;
import static java.util.stream.Collectors.joining;
/**
* This is the global entry point for fat clients.
* The usage for now is only the lookup.
* <p/>
* @author oliver.guenther
*/
//HINT: Name is not perfekt, but we stick with it for now. Alternatives are Lookup, Service, ServiceBus , Valet ...
public class Client {
private final static Logger L = LoggerFactory.getLogger(Client.class);
private final static WorkspaceService workspace = new WorkspaceService();
private final static Map<String, Object> sampleStubs = new HashMap<>();
private final static Map<Class<?>, ? super Object> CACHE = new HashMap<>();
private static final NavigableMap<String, NavigableSet<String>> CLIENT_JNDI_NAME_CACHE = new TreeMap<>();
private static TrayIcon sampleStubTrayIcon;
/**
* Request a context.
* <p>
* @param cause supplying a optional cause, to get, why this context was requested.
* @return a context
*/
public static Context context(String cause) {
// As you can not ask a context if it is close, we have to pick it up always new.
return Objects.requireNonNull(Lookup.getDefault().lookup(Server.class), "Server not found via LookUp, Nature: " + cause).getContext();
}
/**
* Tries to lookup an implementation of the supplied class/interface.
* The discovery goes as follows:
* <ul>
* <li>If the clazz is {@link Workspace} return the static handled instance</li>
* <li>If the clazz doesn't have any of the anotations {@link Remote}, {@link Stateful}, {@link Stateless}, {@link Stateful} then
* use
* <code>{@link Lookup#getDefault() }.lookup(clazz)</code> and ensure the result is not null.
* <ul><li>If the result is null throw a {@link NullPointerException}</li></ul></li>
* <li>Else aquire a {@link Context}, either using an internal cached one or initiating a new one, query the jndi namespace using each element of "Name
* Variations" and java:global/"Project Namespace"/"name variations", returning the frist found implementation
* <ul>
* <li>Name Variations (name is based on clazz.simpleName)
* <ul><li>If clazz doesn't have the annotation {@link Remote} use <i>name</i></li>
* <li>Else:
* <ol>
* <li><i>name</i>, if name ends with <i>OperationRemote</i> or <i>WrapperRemote</i></li>
* <li><i>nameRemote</i>, if name ends with <i>Operation</i> or <i>Wrapper</i></li>
* <li><i>name</i><code>.substring(0, length - 6)</code>(<i>OperationRemote</i> & <i>WrapperRemote</i> & <i>Operation</i> & <i>Wrapper</i>),
* if name ends with <i>Remote</i> and 1 or 2 did not match</li>
* <li><i>name</i>(<i>OperationRemote</i> & <i>WrapperRemote</i> & <i>Operation</i> & <i>Wrapper</i>), if name doesn't end with <i>Remote</i> and 1 or 2 did
* not match</li>
* </ol>
* </li>
* </ul>
* </ul>
* </li>
* </ul>
* <p/>
* @param <T> the type of the resulting instance
* @param clazz the clazz to use as identifier, must not be null.
* @return the fist found implementation.
* @throws NullPointerException if clazz is null.
*/
public static <T> T lookup(Class<T> clazz) throws NullPointerException, IllegalArgumentException {
Objects.requireNonNull(clazz, "clazz is null");
L.info("Looking Up {}", clazz.getName());
//HINT: The Workspace is a special case, we just handle it here. This could be optimized.
if ( clazz.equals(Workspace.class) ) return (T)workspace;
// Loading Cached Values
if ( CACHE.containsKey(clazz) && CACHE.get(clazz) != null ) {
L.debug("Using Cache for {}", clazz.getSimpleName());
return (T)CACHE.get(clazz);
}
// The Sample Stub allows the "injection" of different implementations before any real lookup. This is normaly used ony for tryout and samples.
// This could be done better with a injection framework, but through this implementation, we don't need any server at all.
if ( sampleStubs.containsKey(clazz.getName()) ) return (T)sampleStubs.get(clazz.getName());
// If it doesn't have bean annotations, it must be a local look up.
if ( !hasBeanAnnotation(clazz) ) return Objects.requireNonNull(Lookup.getDefault().lookup(clazz), "No Instance of " + clazz + " via Lookup found");
T result = remoteLookup(clazz);
// fill the cache if enabled (key is set).
if ( CACHE.containsKey(clazz) ) CACHE.put(clazz, CachedProxy.create(clazz, result));
return result;
}
/**
* Allows to ask the Client, if it can find an implementation of the supplied anywhere.
* If the Class is not found in the first place but the Client is a Locale Client, it will try the old way.
* <p>
* @param <T>
* @param clazz the class to look for an implementation
* @return ture if existent.
*/
public static <T> boolean hasFound(Class<T> clazz) {
// Allows the hasFound in a sample environment. A key without a value just means a optional service missing.
if ( sampleStubs.containsKey(clazz.getName()) ) return sampleStubs.get(clazz.getName()) != null;
return CLIENT_JNDI_NAME_CACHE.containsKey(clazz.getName());
}
/**
* Allows the client to cash lookups of specific classes.
* The user should be sure, that these never change.
* <p>
* @param clazz the class to cache.
*/
public static <T> void enableCache(Class<T> clazz) {
CACHE.put(clazz, null);
}
/**
* WARNING: Adds a instance to the sample subs.
* Do not use in productive environment.
* <p/>
* @param <T> type parameter
* @param clazz the clazz as index
* @param t the instance.
*/
public static <T> void addSampleStub(Class<T> clazz, T t) {
sampleStubs.put(clazz.getName(), t);
L.warn("Client lookup Sample Stub filled with {}. If this is happening in the productive system, this is definitivly wrong", clazz.getName());
if ( !SystemTray.isSupported() ) return;
if ( sampleStubTrayIcon == null ) {
Image img = Toolkit.getDefaultToolkit().getImage(loadWarningIcon());
sampleStubTrayIcon = new TrayIcon(img);
sampleStubTrayIcon.addActionListener(e -> JOptionPane.showMessageDialog(null,
"Elements in the Sample Stub\n - " + sampleStubs.keySet().stream().collect(Collectors.joining("\n - ")),
"Elements in the Sample Stub",
JOptionPane.WARNING_MESSAGE));
try {
SystemTray.getSystemTray().add(sampleStubTrayIcon);
sampleStubTrayIcon.displayMessage("Deutsche Warenwirtschaft Sample Stub Active",
"SampleStubs were added to the Client, use only in non productive mode.", WARNING);
} catch (AWTException ex) {
L.error("Could not add to SystemTray", ex);
}
}
}
public static void inspectJndiTree(Context context, String suffix) {
inspectJndiTree(context, suffix, CLIENT_JNDI_NAME_CACHE);
}
/**
* Inspects the JNDI Name Tree and Fills all founded Classes with interfaces into the foundJndiNames Map.
* <p>
* @param context the context
* @param suffix the suffix
* @param cache a mutable list to fill and for recursion
* @return the inspected result
*/
// TODO: It would be great to make this completely functional, but for now it's ok.
public static NavigableMap<String, NavigableSet<String>> inspectJndiTree(Context context, String suffix, NavigableMap<String, NavigableSet<String>> cache) {
try {
NamingEnumeration<NameClassPair> list = context.list(suffix);
while (list != null && list.hasMore()) {
try {
String name = list.next().getName();
if ( name.contains("EjbModule") || name.contains("com.sun.javafx") ) continue; // Ignoring some defaults in the jndi tree
String[] split = name.split("!");
if ( split.length > 1 ) { // Only want implementains of Beans, everything else is ignored.
String key = split[1];
String[] values = {suffix + "/" + split[0], split[0] + "Remote"}; // Second element adding without suffix. @OG whats to know why.
if ( cache.get(key) == null ) cache.put(key, new TreeSet<>());
for (String value : values) {
if ( cache.get(key).add(value) ) L.debug("Storing in cache: key={}, value={}", key, value);
}
}
inspectJndiTree(context, suffix + "/" + name, cache);
} catch (NamingException ex) {
L.warn("Jndi Tree inspection on SubSuffix {} failed: {}", suffix, ex.getMessage());
}
}
} catch (NamingException ex) {
L.warn("Jndi Tree inspection on Suffix {} failed: {}", suffix, ex.getMessage());
}
return cache;
}
// Extracted, Either the inspectJndiTree becomes successful or we must keep the fallback. Nethertheless this is just for fun here.
public static NavigableSet<String> inspectJndiTreeForModuleNames(Context context) {
NavigableSet<String> result = new TreeSet<>();
String suffix = "java:global";
try {
NamingEnumeration<NameClassPair> list = context.list(suffix);
while (list != null && list.hasMore()) {
try {
String name = list.next().getName();
if ( name.contains("EjbModule") || name.contains("com.sun.javafx") ) continue; // Ignoring some values
String[] split = name.split("!");
if ( split.length == 1 ) {
L.debug("Storing in projects {}", suffix + "/" + name);
result.add(suffix + "/" + name);
}
} catch (NamingException ex) {
L.warn("Jndi Tree Module Name inspection on suffix {} failed: {}", suffix, ex.getMessage());
}
}
} catch (NamingException ex) {
L.warn("Jndi Tree Module Name inspection on Suffix {} failed: {}", suffix, ex.getMessage());
}
return result;
}
/**
* Returns null or the Reference to the running (possibly created instance) of the supplied class.
*
* @param <T> The type parameter of the instance.
* @param clazz the class identifying the instance.
* @return the instance itself or null.
* @throws IllegalArgumentException If some error in the process happens, like nothing is found.
*/
private static <T> T remoteLookup(Class<T> clazz) throws IllegalArgumentException {
Context context = context(clazz.getName());
if ( CLIENT_JNDI_NAME_CACHE.isEmpty() ) {
L.info("Running Jndi Tree inspection on Suffix: ''");
inspectJndiTree(context, ""); // Not existing in Local Environment
L.info("Running Jndi Tree inspection on Suffix: 'java:global'");
inspectJndiTree(context, "java:global");
L.info("Running Jndi Tree inspection on Suffix: 'java:module'");
inspectJndiTree(context, "java:module"); // Olli added, Not existing in Local Environment
L.info("Running Jndi Tree inspection on Suffix: 'java:app'");
inspectJndiTree(context, "java:app"); // Olli added, Not existing in Local Environment
L.info("Jndi Tree inspection complete, the clientJndiNameCache has now a size of {}", CLIENT_JNDI_NAME_CACHE.size());
/* Enable only on big problems.
L.debug("Final CLIENT_JNDI_NAME_CACHE {}", CLIENT_JNDI_NAME_CACHE);
System.out.println("Final CLIENT_JNDI_NAME_CACHE");
CLIENT_JNDI_NAME_CACHE.entrySet().forEach(t -> System.out.println(t.getKey() + " - " + t.getValue()));
L.debug("Final Projects {}", DYNAMIC_JAVA_EE_MODULE_NAMES);
System.out.println("Final DYNAMIC_JAVA_EE_MODULE_NAMES");
DYNAMIC_JAVA_EE_MODULE_NAMES.forEach(x -> System.out.println(x));
*/
}
List<String> errors = new ArrayList<>();
String clazzName = clazz.getName();
if ( CLIENT_JNDI_NAME_CACHE.containsKey(clazzName) ) {
for (String name : CLIENT_JNDI_NAME_CACHE.get(clazzName)) {
try {
T result = (T)context.lookup(name);
L.debug("Succesful look up via Cache(key={},value={}) class {}", clazzName, name, result.getClass().getName());
context.close();
return result;
} catch (NamingException ne) {
errors.add("NamingException(jndiName=" + name + ", message=" + ne.getMessage() + ")");
}
}
}
throw new IllegalArgumentException("No Candidate for " + clazz.getSimpleName() + " was found, tried:\n"
+ errors.stream().collect(joining("\n "))
+ "\nUsing Cache:\n"
+ CLIENT_JNDI_NAME_CACHE.entrySet().stream().map(e -> " - " + e.getKey() + " : " + e.getValue()).collect(joining("\n")));
}
static URL loadProperties() {
return Client.class.getResource("project.properties");
}
static URL loadWarningIcon() {
return Client.class.getResource("warning-icon.png");
}
private static boolean hasBeanAnnotation(Class<?> clazz) {
if ( clazz.getAnnotation(Remote.class) != null ) return true;
if ( clazz.getAnnotation(Stateless.class) != null ) return true;
if ( clazz.getAnnotation(Stateful.class) != null ) return true;
return clazz.getAnnotation(Singleton.class) != null;
}
}