emulib.runtime.ContextPool Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of emuLib Show documentation
Show all versions of emuLib Show documentation
emuLib is the run-time library used by emuStudio platform and its plug-ins.
The newest version!
/*
* KISS, YAGNI, DRY
*
* (c) Copyright 2006-2017, Peter Jakubčo
*
* 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 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package emulib.runtime;
import emulib.emustudio.API;
import emulib.plugins.Context;
import emulib.plugins.compiler.CompilerContext;
import emulib.plugins.cpu.CPUContext;
import emulib.plugins.device.DeviceContext;
import emulib.plugins.memory.MemoryContext;
import emulib.runtime.exceptions.AlreadyRegisteredException;
import emulib.runtime.exceptions.ContextNotFoundException;
import emulib.runtime.exceptions.InvalidContextException;
import emulib.runtime.exceptions.InvalidPasswordException;
import emulib.runtime.interfaces.PluginConnections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* This class manages all plug-in contexts.
*
* Plug-ins should register their contexts manually. Other plug-ins that have permissions, can gather contexts by
* querying this pool.
*
* Context pool is not thread safe.
*
*/
public class ContextPool {
private final static Logger LOGGER = LoggerFactory.getLogger(ContextPool.class);
/**
* The following map stores all registered contexts.
*
* Contexts implementing the same context interfaces are stored to the end of the list under the same map key
*/
private final Map> allContexts = new HashMap<>();
/**
* This map represents owners of registered contexts (these are keys).
* It is used for checking the plug-in permissions.
*/
private final Map> contextOwners = new HashMap<>();
/**
* Virtual computer loaded by emuStudio
*/
private final AtomicReference computer = new AtomicReference<>();
private final ReadWriteLock registeringLock = new ReentrantReadWriteLock();
/**
* This method registers plug-in's context interface.
*
* The registration is needed because of contexts centralization. Other plug-ins can get a context by
* querying the pool. Usually, emuStudio does the job during loading of the virtual computer.
*
* Requirements for the context are:
* - It is allowed to implement one and only one context
* - context interface must extend Context interface provided by emuLib
* - context interface must be annotated with @ContextType annotation
*
* @param pluginID owner plugin ID of the context contextsByOwner
* @param context The context object that the plug-in want to register
* @param contextInterface The interface that the context has to implement
* @throws AlreadyRegisteredException Raised when a plug-in tries to register context that is already registered.
* @throws InvalidContextException Raised when a class does not implement given interface, or it is not
* annotated, or if the context interface does not fulfill context requirements.
*/
public void register(long pluginID, Context context, Class extends Context> contextInterface)
throws AlreadyRegisteredException, InvalidContextException {
trustedContext(contextInterface);
String contextHash = computeHash(contextInterface);
// check if the contextInterface is implemented by the context
if (!PluginLoader.doesImplement(context.getClass(), contextInterface)) {
throw new InvalidContextException("Context does not implement context interface");
}
registeringLock.writeLock().lock();
try {
// check if the context is already registered
List contextsByHash = allContexts.get(contextHash);
if (contextsByHash != null) {
// Test if the context instance is already there
if (contextsByHash.contains(context)) {
throw new AlreadyRegisteredException();
}
}
// finally register the context
List contextsByOwner = contextOwners.get(pluginID);
if (contextsByOwner == null) {
contextsByOwner = new ArrayList<>();
contextOwners.put(pluginID, contextsByOwner);
}
contextsByOwner.add(context);
if (contextsByHash == null) {
contextsByHash = new ArrayList<>();
allContexts.put(contextHash, contextsByHash);
}
contextsByHash.add(context);
} finally {
registeringLock.writeLock().unlock();
}
}
/**
* Check if the provided class is a context.
*
* @param contextInterface the context interface
*/
private void trustedContext(Class extends Context> contextInterface) throws InvalidContextException {
if (contextInterface == null) {
throw new InvalidContextException("Interface is null");
}
if (!contextInterface.isInterface()) {
throw new InvalidContextException("Given class is not interface");
}
if (!contextInterface.isAnnotationPresent(emulib.annotations.ContextType.class)) {
throw new InvalidContextException("Interface is not annotated as context");
}
}
/**
* Unregisters all contexts of given context interface.
*
* It will do it only if the plug-in has the permission. The permission is approved if and only if the contexts are
* implemented inside the plug-in.
*
* @param pluginID plugin ID of the context owner
* @param contextInterface the context interface
* @return true if at least one context was unregistered successfully, false otherwise.
* @throws InvalidContextException Raised when context interface is not annotated, or if the context interface does
* not fulfill context requirements.
*
*/
public boolean unregister(long pluginID, Class extends Context> contextInterface) throws InvalidContextException {
trustedContext(contextInterface);
String contextHash = computeHash(contextInterface);
registeringLock.writeLock().lock();
try {
List contextsByOwner = contextOwners.get(pluginID);
if (contextsByOwner == null) {
return false;
}
List contextsByHash = allContexts.get(contextHash);
if (contextsByHash == null) {
return false;
}
Iterator contextIterator = contextsByHash.iterator();
while (contextIterator.hasNext()) {
Context context = contextIterator.next();
if (contextsByOwner.contains(context)) {
contextsByOwner.remove(context);
contextIterator.remove();
}
}
if (contextsByHash.isEmpty()) {
allContexts.remove(contextHash);
}
return true;
} finally {
registeringLock.writeLock().unlock();
}
}
/**
* Set a computer, represented as plug-in connections, loaded by emuStudio.
*
* This method should be called only by the emuStudio.
*
* @param password emuStudio password
* @param computer virtual computer, loaded by emuStudio
* @return true if computer was set successfully; false otherwise.
* @throws InvalidPasswordException if the password was incorrect
*/
public boolean setComputer(String password, PluginConnections computer) throws InvalidPasswordException {
API.testPassword(password);
this.computer.set(computer);
return true;
}
/**
* Clear the context instance.
*
* @param password emuStudio password
* @throws InvalidPasswordException if the password was incorrect
*/
public void clearAll(String password) throws InvalidPasswordException {
API.testPassword(password);
this.computer.set(null);
registeringLock.writeLock().lock();
try {
allContexts.clear();
contextOwners.clear();
} finally {
registeringLock.writeLock().unlock();
}
}
/**
* Get plug-in context.
*
* @param Specific context type
* @param pluginID ID of requesting plug-in
* @param contextInterface wanted context interface (implemented by the plug-in)
* @param index
* the index if more than one context are found. If -1 is
* provided, any matched context is used
* @return requested context, which plugin owner != pluginId
* @throws InvalidContextException
* if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getContext(long pluginID, Class contextInterface,
int index) throws InvalidContextException, ContextNotFoundException {
trustedContext(contextInterface);
registeringLock.readLock().lock();
try {
// find the requested context
List contextsByHash = allContexts.get(computeHash(contextInterface));
if ((contextsByHash == null) || contextsByHash.isEmpty()) {
throw new ContextNotFoundException("Context "
+ contextInterface
+ " is not found in registered contexts list.");
}
LOGGER.debug("Matching context " + contextInterface + " from " + contextsByHash.size() + " options...");
// find context based on contextID
int j = 0;
for (Context context : contextsByHash) {
if (checkPermission(pluginID, context)) {
if ((index == -1) || (j == index)) {
LOGGER.debug("Found context with index " + j);
return (T)context;
}
}
j++;
}
throw new ContextNotFoundException("The plugin with ID "
+ pluginID
+ " has no permission to access context "
+ contextInterface
);
} finally {
registeringLock.readLock().unlock();
}
}
/**
* Get registered CPU context.
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the CPU in the abstract schema.
*
* If the CPU has more than one context implementing required context interface, the first one is returned. To
* access other ones, use extended version of the method.
*
* This method call is equivalent to the call of getCPUContext(pluginID, contextInterface, -1);
*
* @param Specific CPU context
* @param pluginID plug-in requesting the CPU context
* @param contextInterface Interface of the context
* @return CPUContext object if it is found and the plug-in has the permission to access it; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getCPUContext(long pluginID, Class contextInterface)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, -1);
}
/**
* Get registered CPU context (extended version).
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the CPU in the abstract schema.
*
* If the CPU has more than one context implementing required context interface, it returns context indexed by index
* parameter.
*
* @param Specific CPU Context
* @param pluginID plug-in requesting the CPU context
* @param contextInterface Interface of the context
* @param index 0-based the order of the context if they are more than one. Does nothing if the index is out of
* the bounds. If index is -1, it uses any found context.
* @return CPUContext object if it is found and the plug-in has the permission; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getCPUContext(long pluginID, Class contextInterface, int index)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, index);
}
/**
* Get registered compiler context.
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the compiler in the abstract schema.
*
* If the compiler has more than one context implementing required context interface, the first one is returned. To
* access other ones, use extended version of the method.
*
* This method call is equivalent to the call of getCompilerContext(pluginID, contextInterface, -1);
*
* @param Specific compiler context
* @param pluginID plug-in requesting the compiler context
* @param contextInterface Interface of the context, if requesting plugin has permission to acccess it
* @return CompilerContext object if it is found and the plug-in has the permission to access it; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getCompilerContext(long pluginID, Class contextInterface)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, -1);
}
/**
* Get registered compiler context (extended version).
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the compiler in the abstract schema.
*
* If the compiler has more than one context implementing required context interface, it returns context indexed by
* index parameter.
*
* @param Specific compiler context
* @param pluginID plug-in requesting the Compiler context
* @param contextInterface Interface of the context
* @param index the order of the context if they are more than one. Does nothing if the index is out of bounds.
* If the index is -1, it uses any found context.
* @return CompilerContext object if it is found and the plug-in has the permission to access it; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getCompilerContext(long pluginID, Class contextInterface, int index)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, index);
}
/**
* Get registered memory context.
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the memory in the abstract schema.
*
* If the memory has more than one context implementing required context interface, the first one is returned. To
* access other ones, use extended version of the method.
*
* This method call is equivalent to the call of getMemoryContext(pluginID, contextInterface, -1);
*
* @param Specific memory context
* @param pluginID plug-in requesting the memory context
* @param contextInterface Interface of the context
* @return MemoryContext object if it is found and the plug-in has the permission to access it; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getMemoryContext(long pluginID, Class contextInterface)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, -1);
}
/**
* Get registered memory context (extended version).
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the memory in the abstract schema.
*
* If the memory has more than one context implementing required context interface, it returns context indexed by
* index parameter.
*
* @param Specific memory context
* @param pluginID plug-in requesting the memory context
* @param contextInterface Interface of the context
* @param index the index of the context if they are more than one. Does nothing if the index is out of bounds.
* If the index is -1, it uses any found context.
* @return MemoryContext object if it is found and the plug-in has the permission to access it; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getMemoryContext(long pluginID, Class contextInterface, int index)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, index);
}
/**
* Get registered device context.
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the device in the abstract schema.
*
* If the device has more than one context implementing required context interface, the first one is returned. To
* access other ones, use extended version of the method.
*
* This method call is equivalent to the call of getDeviceContext(pluginID, contextInterface, -1);
*
* @param Specific device context
* @param pluginID plug-in requesting the device context
* @param contextInterface Interface of the context
* @return DeviceContext object if it is found and the plug-in has the permission to access it; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getDeviceContext(long pluginID, Class contextInterface)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, -1);
}
/**
* Get registered device context (extended version).
*
* If plug-in doesn't have the permission to access it, return null. The permission is approved, when the
* plug-in is connected to the device in the abstract schema.
*
* If the device has more than one context implementing required context interface, it returns context indexed by
* index parameter.
*
* @param Specific device context
* @param pluginID plug-in requesting the device context
* @param contextInterface Interface of the context
* @param index index of the context implementation. Does nothing if the index is out of bounds.
* If the index is -1, it uses any found context.
* @return DeviceContext object if it is found and the plug-in has the permission to access it; null otherwise
* @throws InvalidContextException if the context interface does not fulfil context requirements
* @throws ContextNotFoundException
* if the context does not exist or the plug-in is not allowed to get it
*/
public T getDeviceContext(long pluginID, Class contextInterface, int index)
throws InvalidContextException, ContextNotFoundException {
return getContext(pluginID, contextInterface, index);
}
private Long findContextOwner(Context context) {
Long contextOwner = null;
for (Map.Entry> owner : contextOwners.entrySet()) {
List contextsByOwner = owner.getValue();
assert (contextsByOwner != null);
if (contextsByOwner.contains(context)) {
contextOwner = owner.getKey();
break;
}
}
return contextOwner;
}
/**
* This method check if the plug-in has the permission to access specified context.
*
* The permission is granted if and only if the context is connected to the plug-in inside virtual computer.
*
* Note: it can be called only when registeringLock is held.
*
* @param pluginID plug-in to check
* @param context requested context
* @return true if the plug-in is approved to access the context; false otherwise
*/
private boolean checkPermission(long pluginID, Context context) {
// at first check if the pluginID == hash code of emuStudio password
if (API.testPassword(pluginID)) {
return true;
}
// check if it is possible to check the plug-in for the permission
PluginConnections tmpComputer = computer.get();
if (tmpComputer == null) {
LOGGER.debug("Plugin with ID=" + pluginID + " cannot have access to context " + context + ": Computer is not set.");
return false;
}
// first it must be found the contextsByOwner of the ContextPool.
Long contextOwner = findContextOwner(context);
// THIS is the permission check
LOGGER.debug("Checking permission of plugin with ID=" + pluginID + " to context owner with ID=" + contextOwner
+ " (" + context + ")");
return tmpComputer.isConnected(pluginID, contextOwner);
}
/**
* Compute emuStudio-specific hash of the context interface.
* The name of the interface is not important, only method names and their
* signatures.
*
* The final processing uses SHA-1 method.
*
* @param contextInterface interface to compute hash of
* @return SHA-1 hash string of the interface
*/
public static String computeHash(Class extends Context> contextInterface) {
List contextMethods = Arrays.asList(contextInterface.getMethods());
Collections.sort(contextMethods, (m1, m2) -> m1.getName().compareTo(m2.getName()));
String hash = "";
for (Method method : contextMethods.toArray(new Method[0])) {
hash += method.getGenericReturnType().toString() + " " + method.getName() + "(";
for (Class> param : method.getParameterTypes()) {
hash += param.getName() + ",";
}
hash += ");";
}
try {
return SHA1(hash);
} catch(NoSuchAlgorithmException | UnsupportedEncodingException e) {
LOGGER.error("Could not compute hash for interface " + contextInterface, e);
return null;
}
}
/**
* Compute SHA-1 hash string.
*
* Letters in the hash string will be in upper-case.
*
* @param text Data to make hash from
* @return SHA-1 hash Hexadecimal string, null if there was some error
* @throws java.security.NoSuchAlgorithmException self-descriptive
* @throws java.io.UnsupportedEncodingException self-descriptive
*/
public static String SHA1(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException {
MessageDigest md;
md = MessageDigest.getInstance("SHA-1");
byte[] sha1hash;
md.update(text.getBytes("iso-8859-1"), 0, text.length());
sha1hash = md.digest();
return RadixUtils.convertToRadix(sha1hash, 16, false);
}
}