package org.jolokia.history;
import java.io.*;
import java.util.*;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.jolokia.request.*;
import org.jolokia.util.RequestType;
import org.json.simple.JSONObject;
/*
* 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.
*/
/**
* Store for remembering values which has been fetched through a previous
* request.
*
* @author roland
* @since Jun 12, 2009
*/
@SuppressWarnings("IS2_INCONSISTENT_SYNC") // FindBugs gets confused with inner classes accessing objects in the parent
public class HistoryStore {
// Hard limit for number of entries for a single history track
private int globalMaxEntries;
private Map<HistoryKey, HistoryEntry> historyStore;
private Map<HistoryKey, HistoryLimit> patterns;
// Keys used in JSON representation
private static final String KEY_HISTORY = "history";
private static final String KEY_VALUE = "value";
private static final String KEY_TIMESTAMP = "timestamp";
private Map<RequestType,HistoryUpdater> historyUpdaters = new HashMap<RequestType, HistoryUpdater>();
/**
* Constructor for a history store
*
* @param pTotalMaxEntries number of entries to hold at max. Even when configured, this maximum can not
* be overwritten. This is a hard limit.
*/
public HistoryStore(int pTotalMaxEntries) {
globalMaxEntries = pTotalMaxEntries;
historyStore = new HashMap<HistoryKey, HistoryEntry>();
patterns = new HashMap<HistoryKey, HistoryLimit>();
initHistoryUpdaters();
}
/**
* Get the maximum number of entries stored.
*
* @return the maximum number of entries
*/
public synchronized int getGlobalMaxEntries() {
return globalMaxEntries;
}
/**
* Set the global maximum limit for history entries.
*
* @param pGlobalMaxEntries limit
*/
public synchronized void setGlobalMaxEntries(int pGlobalMaxEntries) {
globalMaxEntries = pGlobalMaxEntries;
// Refresh all entries
for (HistoryEntry entry : historyStore.values()) {
entry.setMaxEntries(globalMaxEntries);
}
}
/**
* Configure the history length for a specific entry. If the length
* is 0 disable history for this key. Please note, that this method might change the limit
* object so the ownership of this object goes over to the callee.
*
* @param pKey history key
* @param pHistoryLimit limit to apply or <code>null</code> if no history should be recored for this entry
*/
public synchronized void configure(HistoryKey pKey, HistoryLimit pHistoryLimit) {
// Remove entries if set to null
if (pHistoryLimit == null) {
removeEntries(pKey);
return;
}
HistoryLimit limit = pHistoryLimit.respectGlobalMaxEntries(globalMaxEntries);
if (pKey.isMBeanPattern()) {
patterns.put(pKey,limit);
// Trim all already stored keys
for (HistoryKey key : historyStore.keySet()) {
if (pKey.matches(key)) {
HistoryEntry entry = historyStore.get(key);
entry.setLimit(limit);
}
}
} else {
HistoryEntry entry = historyStore.get(pKey);
if (entry != null) {
entry.setLimit(limit);
} else {
entry = new HistoryEntry(limit);
historyStore.put(pKey,entry);
}
}
}
/**
* Reset the complete store.
*/
public synchronized void reset() {
historyStore = new HashMap<HistoryKey, HistoryEntry>();
patterns = new HashMap<HistoryKey, HistoryLimit>();
}
/**
* Update the history store with the value of an an read, write or execute operation. Also, the timestamp
* of the insertion is recorded. Also, the recorded history values are added to the given json value.
*
* @param pJmxReq request for which an entry should be added in this history store
* @param pJson the JSONObject to which to add the history.
*/
public synchronized void updateAndAdd(JmxRequest pJmxReq, JSONObject pJson) {
long timestamp = System.currentTimeMillis() / 1000;
pJson.put(KEY_TIMESTAMP,timestamp);
RequestType type = pJmxReq.getType();
HistoryUpdater updater = historyUpdaters.get(type);
if (updater != null) {
updater.updateHistory(pJson,pJmxReq,timestamp);
}
}
/**
* Get the size of this history store in bytes
*
* @return size in bytes
*/
public synchronized int getSize() {
try {
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
ObjectOutputStream oOut = new ObjectOutputStream(bOut);
oOut.writeObject(historyStore);
bOut.close();
return bOut.size();
} catch (IOException e) {
throw new IllegalStateException("Cannot serialize internal store: " + e,e);
}
}
// =======================================================================================================
// Interface for updating a history entry for a certain type
/**
* Internal interface used for updating this store
*
* @param <R> request type
*/
interface HistoryUpdater<R extends JmxRequest> {
/**
* Update history
*
* @param pJson the result of the request
* @param request request leading to the result
* @param pTimestamp timestamp when the request was executed
*/
void updateHistory(JSONObject pJson,R request,long pTimestamp);
}
// A set of updaters which are dispatched to for certain request types
private void initHistoryUpdaters() {
historyUpdaters.put(RequestType.EXEC,
new HistoryUpdater<JmxExecRequest>() {
/** {@inheritDoc} */
public void updateHistory(JSONObject pJson,JmxExecRequest request, long pTimestamp) {
HistoryEntry entry = historyStore.get(new HistoryKey(request));
if (entry != null) {
synchronized(entry) {
pJson.put(KEY_HISTORY,entry.jsonifyValues());
entry.add(pJson.get(KEY_VALUE),pTimestamp);
}
}
}
});
historyUpdaters.put(RequestType.WRITE,
new HistoryUpdater<JmxWriteRequest>() {
/** {@inheritDoc} */
public void updateHistory(JSONObject pJson,JmxWriteRequest request, long pTimestamp) {
HistoryEntry entry = historyStore.get(new HistoryKey(request));
if (entry != null) {
synchronized(entry) {
pJson.put(KEY_HISTORY,entry.jsonifyValues());
entry.add(request.getValue(),pTimestamp);
}
}
}
});
historyUpdaters.put(RequestType.READ,
new HistoryUpdater<JmxReadRequest>() {
/** {@inheritDoc} */
public void updateHistory(JSONObject pJson,JmxReadRequest request, long pTimestamp) {
updateReadHistory(request,pJson,pTimestamp);
}
});
}
// Remove entries
private void removeEntries(HistoryKey pKey) {
if (pKey.isMBeanPattern()) {
patterns.remove(pKey);
List<HistoryKey> toRemove = new ArrayList<HistoryKey>();
for (HistoryKey key : historyStore.keySet()) {
if (pKey.matches(key)) {
toRemove.add(key);
}
}
// Avoid concurrent modification exceptions
for (HistoryKey key : toRemove) {
historyStore.remove(key);
}
} else {
HistoryEntry entry = historyStore.get(pKey);
if (entry != null) {
historyStore.remove(pKey);
}
}
}
// Update potentially multiple history entries for a READ request which could
// return multiple values with a single request
private void updateReadHistory(JmxReadRequest pJmxReq, JSONObject pJson, long pTimestamp) {
ObjectName name = pJmxReq.getObjectName();
if (name.isPattern()) {
// We have a pattern and hence a value structure
// of bean -> attribute_key -> attribute_value
Map<String,Object> values = (Map<String, Object>) pJson.get(KEY_VALUE);
// Can be null if used with path and no single match occurred
if (values != null) {
JSONObject history = updateHistoryForPatternRead(pJmxReq, pTimestamp, values);
if (history.size() > 0) {
pJson.put(KEY_HISTORY,history);
}
}
} else if (pJmxReq.isMultiAttributeMode() || !pJmxReq.hasAttribute()) {
// Multiple attributes, but a single bean.
// Value has the following structure:
// attribute_key -> attribute_value
JSONObject history = addMultipleAttributeValues(
pJmxReq,
((Map<String, Object>) pJson.get(KEY_VALUE)),
pJmxReq.getObjectNameAsString(),
pTimestamp);
if (history.size() > 0) {
pJson.put(KEY_HISTORY,history);
}
} else {
// Single attribute, single bean. Value is the attribute_value
// itself.
addAttributeFromSingleValue(pJson,
new HistoryKey(pJmxReq), KEY_HISTORY,
pJson.get(KEY_VALUE),
pTimestamp);
}
}
private JSONObject updateHistoryForPatternRead(JmxReadRequest pJmxReq, long pTimestamp, Map<String, Object> pValues) {
JSONObject history = new JSONObject();
List<String> pathParts = pJmxReq.getPathParts();
if (pathParts != null && pathParts.size() == 1) {
return updateHistoryForPatternReadWithMBeanAsPath(pJmxReq, pTimestamp, pValues);
}
for (Map.Entry<String,Object> beanEntry : pValues.entrySet()) {
JSONObject beanHistory = null;
String beanName = beanEntry.getKey();
Object value = beanEntry.getValue();
if (pathParts != null && pathParts.size() == 2) {
beanHistory = addPathFilteredAttributeValue(pJmxReq, pTimestamp, beanName, value);
}
if (value instanceof Map) {
beanHistory =
addMultipleAttributeValues(
pJmxReq,
((Map<String, Object>) beanEntry.getValue()),
beanName,
pTimestamp);
}
if (beanHistory != null && beanHistory.size() > 0) {
history.put(beanName, beanHistory);
}
}
return history;
}
private JSONObject addPathFilteredAttributeValue(JmxReadRequest pJmxReq, long pTimestamp, String pBeanName, Object pValue) {
// value
String attribute = pJmxReq.getPathParts().get(1);
HistoryKey key = createHistoryKey(pJmxReq, pBeanName,attribute,pJmxReq.getPath());
return addAttributeFromSingleValue(key,attribute,pValue,pTimestamp);
}
private JSONObject updateHistoryForPatternReadWithMBeanAsPath(JmxReadRequest pJmxReq, long pTimestamp, Map<String, Object> pValues) {
// It the content of the MBean itself. MBean name is the first the single path part
String beanName = pJmxReq.getPathParts().get(0);
JSONObject ret = new JSONObject();
JSONObject beanHistory = addMultipleAttributeValues(
pJmxReq,
pValues,
beanName,
pTimestamp);
if (beanHistory.size() > 0) {
ret.put(beanName, beanHistory);
}
return ret;
}
private JSONObject addMultipleAttributeValues(JmxRequest pJmxReq, Map<String, Object> pAttributesMap,
String pBeanName, long pTimestamp) {
JSONObject ret = new JSONObject();
for (Map.Entry<String,Object> attrEntry : pAttributesMap.entrySet()) {
String attrName = attrEntry.getKey();
Object value = attrEntry.getValue();
String path = pJmxReq.getPath();
HistoryKey key = createHistoryKey(pJmxReq, pBeanName, attrName, path);
addAttributeFromSingleValue(ret,
key,
attrName,
value,
pTimestamp);
}
return ret;
}
private HistoryKey createHistoryKey(JmxRequest pJmxReq, String pBeanName, String pAttrName, String pPath) {
HistoryKey key;
try {
String target = pJmxReq.getTargetConfig() != null ? pJmxReq.getTargetConfig().getUrl() : null;
key = new HistoryKey(pBeanName, pAttrName, pPath,target);
} catch (MalformedObjectNameException e) {
// Shouldn't occur since we get the MBeanName from a JMX operation's result. However,
// we will rethrow it just in case
throw new IllegalArgumentException("Can not parse MBean name " + pBeanName,e);
}
return key;
}
// Return a fresh map
private JSONObject addAttributeFromSingleValue(HistoryKey pKey, String pAttrName, Object pValue, long pTimestamp) {
HistoryEntry entry = getEntry(pKey,pValue,pTimestamp);
return entry != null ?
addToHistoryEntryAndGetCurrentHistory(new JSONObject(), entry, pAttrName, pValue, pTimestamp) :
null;
}
// Use an existing map
private void addAttributeFromSingleValue(JSONObject pHistMap, HistoryKey pKey, String pAttrName,
Object pValue, long pTimestamp) {
HistoryEntry entry = getEntry(pKey, pValue, pTimestamp);
if (entry != null) {
addToHistoryEntryAndGetCurrentHistory(pHistMap, entry, pAttrName, pValue, pTimestamp);
}
}
private JSONObject addToHistoryEntryAndGetCurrentHistory(JSONObject pHistMap, HistoryEntry pEntry, String pAttrName,
Object pValue, long pTimestamp) {
synchronized (pEntry) {
pHistMap.put(pAttrName, pEntry.jsonifyValues());
pEntry.add(pValue, pTimestamp);
}
return pHistMap;
}
private synchronized HistoryEntry getEntry(HistoryKey pKey,Object pValue,long pTimestamp) {
HistoryEntry entry = historyStore.get(pKey);
if (entry != null) {
return entry;
}
// Now try all known patterns and add lazily the key
for (HistoryKey key : patterns.keySet()) {
if (key.matches(pKey)) {
entry = new HistoryEntry(patterns.get(key));
entry.add(pValue,pTimestamp);
historyStore.put(pKey,entry);
return entry;
}
}
return null;
}
}