package org.jolokia.backend; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.*; import javax.management.*; import org.jolokia.backend.executor.NotChangedException; import org.jolokia.config.ConfigKey; import org.jolokia.config.Configuration; import org.jolokia.converter.Converters; import org.jolokia.converter.json.JsonConvertOptions; import org.jolokia.detector.ServerHandle; import org.jolokia.discovery.AgentDetails; import org.jolokia.discovery.AgentDetailsHolder; import org.jolokia.history.HistoryStore; import org.jolokia.request.JmxRequest; import org.jolokia.restrictor.AllowAllRestrictor; import org.jolokia.restrictor.Restrictor; import org.jolokia.util.*; import org.json.simple.JSONObject; import static org.jolokia.config.ConfigKey.*; /* * Copyright 2009-2013 Roland Huss * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Backendmanager for dispatching to various backends based on a given * {@link JmxRequest} * * @author roland * @since Nov 11, 2009 */ public class BackendManager implements AgentDetailsHolder { // Dispatches request to local MBeanServer private LocalRequestDispatcher localDispatcher; // Converter for converting various attribute object types // a JSON representation private Converters converters; // Hard limits for conversion private JsonConvertOptions.Builder convertOptionsBuilder; // Handling access restrictions private Restrictor restrictor; // History handler private HistoryStore historyStore; // Storage for storing debug information private DebugStore debugStore; // Loghandler for dispatching logs private LogHandler logHandler; // List of RequestDispatchers to consult private List<RequestDispatcher> requestDispatchers; // Initialize used for late initialization // ("volatile: because we use double-checked locking later on // --> http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html) private volatile Initializer initializer; // Details about the agent inclding the server handle private AgentDetails agentDetails; /** * Construct a new backend manager with the given configuration and which allows * every operation (no restrictor) * * @param pConfig configuration used for tuning this handler's behaviour * @param pLogHandler logger */ public BackendManager(Configuration pConfig, LogHandler pLogHandler) { this(pConfig, pLogHandler, null); } /** * Construct a new backend manager with the given configuration. * * @param pConfig configuration used for tuning this handler's behaviour * @param pLogHandler logger * @param pRestrictor a restrictor for limiting access. Can be null in which case every operation is allowed */ public BackendManager(Configuration pConfig, LogHandler pLogHandler, Restrictor pRestrictor) { this(pConfig,pLogHandler,pRestrictor,false); } /** * Construct a new backend manager with the given configuration. * * @param pConfig configuration map used for tuning this handler's behaviour * @param pLogHandler logger * @param pRestrictor a restrictor for limiting access. Can be null in which case every operation is allowed * @param pLazy whether the initialisation should be done lazy */ public BackendManager(Configuration pConfig, LogHandler pLogHandler, Restrictor pRestrictor, boolean pLazy) { // Access restrictor restrictor = pRestrictor != null ? pRestrictor : new AllowAllRestrictor(); // Log handler for putting out debug logHandler = pLogHandler; // Details about the agent, used for discovery agentDetails = new AgentDetails(pConfig); if (pLazy) { initializer = new Initializer(pConfig); } else { init(pConfig); initializer = null; } } /** * Handle a single JMXRequest. The response status is set to 200 if the request * was successful * * @param pJmxReq request to perform * @return the already converted answer. * @throws InstanceNotFoundException * @throws AttributeNotFoundException * @throws ReflectionException * @throws MBeanException */ public JSONObject handleRequest(JmxRequest pJmxReq) throws InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException, IOException { lazyInitIfNeeded(); boolean debug = isDebug(); long time = 0; if (debug) { time = System.currentTimeMillis(); } JSONObject json; try { json = callRequestDispatcher(pJmxReq); // Update global history store, add timestamp and possibly history information to the request historyStore.updateAndAdd(pJmxReq,json); json.put("status",200 /* success */); } catch (NotChangedException exp) { // A handled indicates that its value hasn't changed. We return an status with //"304 Not Modified" similar to the HTTP status code (http://en.wikipedia.org/wiki/HTTP_status) json = new JSONObject(); json.put("request",pJmxReq.toJSON()); json.put("status",304); json.put("timestamp",System.currentTimeMillis() / 1000); } if (debug) { debug("Execution time: " + (System.currentTimeMillis() - time) + " ms"); debug("Response: " + json); } return json; } /** * Convert a Throwable to a JSON object so that it can be included in an error response * * @param pExp throwable to convert * @param pJmxReq the request from where to take the serialization options * @return the exception. */ public Object convertExceptionToJson(Throwable pExp, JmxRequest pJmxReq) { JsonConvertOptions opts = getJsonConvertOptions(pJmxReq); try { JSONObject expObj = (JSONObject) converters.getToJsonConverter().convertToJson(pExp,null,opts); return expObj; } catch (AttributeNotFoundException e) { // Cannot happen, since we dont use a path return null; } } /** * Remove MBeans */ public void destroy() { try { localDispatcher.destroy(); } catch (JMException e) { error("Cannot unregister MBean: " + e,e); } } /** * Check whether remote access from the given client is allowed. * * @param pRemoteHost remote host to check against. Can be null if no reverse lookup is configured. * @param pRemoteAddr alternative IP address * @return true if remote access is allowed */ public boolean isRemoteAccessAllowed(String pRemoteHost, String pRemoteAddr) { return restrictor.isRemoteAccessAllowed(pRemoteHost != null ? new String[] { pRemoteHost, pRemoteAddr } : new String[] { pRemoteAddr }); } /** * Check whether CORS access is allowed for the given origin. * * @param pOrigin origin URL which needs to be checked * @param pStrictChecking whether to a strict check (i.e. server side check) * @return true if if cors access is allowed */ public boolean isOriginAllowed(String pOrigin,boolean pStrictChecking) { return restrictor.isOriginAllowed(pOrigin, pStrictChecking); } /** * Log at info level * * @param msg to log */ public void info(String msg) { logHandler.info(msg); if (debugStore != null) { debugStore.log(msg); } } /** * Log at debug level * * @param msg message to log */ public void debug(String msg) { logHandler.debug(msg); if (debugStore != null) { debugStore.log(msg); } } /** * Log at error level. * * @param message message to log * @param t ecxeption occured */ public void error(String message, Throwable t) { // Must not be final so that we can mock it in EasyMock for our tests logHandler.error(message, t); if (debugStore != null) { debugStore.log(message, t); } } /** * Whether debug is switched on * * @return true if debug is switched on */ public boolean isDebug() { return debugStore != null && debugStore.isDebug(); } /** * Get the details for the agent which can be updated or used * * @return agent details */ public AgentDetails getAgentDetails() { return agentDetails; } // ========================================================================================================== // Initialized used for late initialisation as it is required for the agent when used // as startup options private final class Initializer { private Configuration config; private Initializer(Configuration pConfig) { config = pConfig; } void init() { BackendManager.this.init(config); } } // Run initialized if not already done private void lazyInitIfNeeded() { if (initializer != null) { synchronized (this) { if (initializer != null) { initializer.init(); initializer = null; } } } } // Initialize this object; private void init(Configuration pConfig) { // Central objects converters = new Converters(); initLimits(pConfig); // Create and remember request dispatchers localDispatcher = new LocalRequestDispatcher(converters, restrictor, pConfig, logHandler); ServerHandle serverHandle = localDispatcher.getServerHandle(); requestDispatchers = createRequestDispatchers(pConfig != null ? pConfig.get(DISPATCHER_CLASSES) : null, converters,serverHandle,restrictor); requestDispatchers.add(localDispatcher); // Backendstore for remembering agent state initMBeans(pConfig); agentDetails.setServerInfo(serverHandle.getVendor(),serverHandle.getProduct(),serverHandle.getVersion()); } private void initLimits(Configuration pConfig) { // Max traversal depth if (pConfig != null) { convertOptionsBuilder = new JsonConvertOptions.Builder( getNullSaveIntLimit(pConfig.get(MAX_DEPTH)), getNullSaveIntLimit(pConfig.get(MAX_COLLECTION_SIZE)), getNullSaveIntLimit(pConfig.get(MAX_OBJECTS)) ); } else { convertOptionsBuilder = new JsonConvertOptions.Builder(); } } private int getNullSaveIntLimit(String pValue) { return pValue != null ? Integer.parseInt(pValue) : 0; } // Construct configured dispatchers by reflection. Returns always // a list, an empty one if no request dispatcher should be created private List<RequestDispatcher> createRequestDispatchers(String pClasses, Converters pConverters, ServerHandle pServerHandle, Restrictor pRestrictor) { List<RequestDispatcher> ret = new ArrayList<RequestDispatcher>(); if (pClasses != null && pClasses.length() > 0) { String[] names = pClasses.split("\\s*,\\s*"); for (String name : names) { ret.add(createDispatcher(name, pConverters, pServerHandle, pRestrictor)); } } return ret; } // Create a single dispatcher private RequestDispatcher createDispatcher(String pDispatcherClass, Converters pConverters, ServerHandle pServerHandle, Restrictor pRestrictor) { try { Class clazz = ClassUtil.classForName(pDispatcherClass, getClass().getClassLoader()); if (clazz == null) { throw new IllegalArgumentException("Couldn't lookup dispatcher " + pDispatcherClass); } Constructor constructor = clazz.getConstructor(Converters.class, ServerHandle.class, Restrictor.class); return (RequestDispatcher) constructor.newInstance(pConverters, pServerHandle, pRestrictor); } catch (NoSuchMethodException e) { throw new IllegalArgumentException("Class " + pDispatcherClass + " has invalid constructor: " + e,e); } catch (IllegalAccessException e) { throw new IllegalArgumentException("Constructor of " + pDispatcherClass + " couldn't be accessed: " + e,e); } catch (InvocationTargetException e) { throw new IllegalArgumentException(e); } catch (InstantiationException e) { throw new IllegalArgumentException(pDispatcherClass + " couldn't be instantiated: " + e,e); } } // call the an appropriate request dispatcher private JSONObject callRequestDispatcher(JmxRequest pJmxReq) throws InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException, IOException, NotChangedException { Object retValue = null; boolean useValueWithPath = false; boolean found = false; for (RequestDispatcher dispatcher : requestDispatchers) { if (dispatcher.canHandle(pJmxReq)) { retValue = dispatcher.dispatchRequest(pJmxReq); useValueWithPath = dispatcher.useReturnValueWithPath(pJmxReq); found = true; break; } } if (!found) { throw new IllegalStateException("Internal error: No dispatcher found for handling " + pJmxReq); } JsonConvertOptions opts = getJsonConvertOptions(pJmxReq); Object jsonResult = converters.getToJsonConverter() .convertToJson(retValue, useValueWithPath ? pJmxReq.getPathParts() : null, opts); JSONObject jsonObject = new JSONObject(); jsonObject.put("value",jsonResult); jsonObject.put("request",pJmxReq.toJSON()); return jsonObject; } private JsonConvertOptions getJsonConvertOptions(JmxRequest pJmxReq) { return convertOptionsBuilder. maxDepth(pJmxReq.getParameterAsInt(ConfigKey.MAX_DEPTH)). maxCollectionSize(pJmxReq.getParameterAsInt(ConfigKey.MAX_COLLECTION_SIZE)). maxObjects(pJmxReq.getParameterAsInt(ConfigKey.MAX_OBJECTS)). faultHandler(pJmxReq.getValueFaultHandler()). useAttributeFilter(pJmxReq.getPathParts() != null). build(); } // init various application wide stores for handling history and debug output. private void initMBeans(Configuration pConfig) { int maxEntries = pConfig.getAsInt(HISTORY_MAX_ENTRIES); int maxDebugEntries = pConfig.getAsInt(DEBUG_MAX_ENTRIES); historyStore = new HistoryStore(maxEntries); debugStore = new DebugStore(maxDebugEntries, pConfig.getAsBoolean(DEBUG)); try { localDispatcher.initMBeans(historyStore, debugStore); } catch (NotCompliantMBeanException e) { intError("Error registering config MBean: " + e, e); } catch (MBeanRegistrationException e) { intError("Cannot register MBean: " + e, e); } catch (MalformedObjectNameException e) { intError("Invalid name for config MBean: " + e, e); } } // Final private error log for use in the constructor above private void intError(String message,Throwable t) { logHandler.error(message, t); debugStore.log(message, t); } }