/** * Copyright (c) 2009-2011 VMware, Inc. All Rights Reserved. * * 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. */ package com.springsource.insight.plugin.files.tracker; import java.io.Closeable; import java.io.File; import java.io.Serializable; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Level; import org.aspectj.lang.JoinPoint; import com.springsource.insight.collection.OperationCollectionAspectSupport; import com.springsource.insight.collection.OperationCollectionUtil; import com.springsource.insight.collection.OperationCollector; import com.springsource.insight.intercept.InterceptConfiguration; import com.springsource.insight.intercept.operation.Operation; import com.springsource.insight.intercept.plugin.CollectionSettingName; import com.springsource.insight.intercept.plugin.CollectionSettingsRegistry; import com.springsource.insight.intercept.plugin.CollectionSettingsUpdateListener; import com.springsource.insight.intercept.trace.FrameBuilder; import com.springsource.insight.util.StringFormatterUtils; import com.springsource.insight.util.StringUtil; import com.springsource.insight.util.logging.InsightLogManager; import com.springsource.insight.util.logging.InsightLogger; /** * */ public abstract class AbstractFilesTrackerAspectSupport extends OperationCollectionAspectSupport { private static final InterceptConfiguration configuration = InterceptConfiguration.getInstance(); public static final int DEFAULT_FILE_CACHE_SIZE = 256; public static final boolean DEFAULT_SUPPRESS_MAPPINGS_WARNINGS_VALUE = true; /** * Default logging {@link Level} for tracker */ public static final Level DEFAULT_LEVEL = Level.OFF; static final FilesCache filesCache = new FilesCache(DEFAULT_FILE_CACHE_SIZE); /** * A {@link Map} of the currently open files - key=the owning instance {@link CacheKey}, * value=the file path */ static Map<CacheKey, String> trackedFilesMap = Collections.synchronizedMap(filesCache); private static volatile Level logLevel = DEFAULT_LEVEL; protected static final CollectionSettingName MAX_TRACKED_FILES_SETTING = new CollectionSettingName("max.tracked.files", FilesTrackerPluginRuntimeDescriptor.PLUGIN_NAME, "Controls the number of concurrently tracked files (default=" + DEFAULT_FILE_CACHE_SIZE + ")"); protected static final CollectionSettingName MAPPINGS_TRACKER_LOG_SETTING = new CollectionSettingName("mappings.tracker.loglevel", FilesTrackerPluginRuntimeDescriptor.PLUGIN_NAME, "The java.util.logging.Level value to use for logging tracked files (default=" + DEFAULT_LEVEL + ")"); // register a collection setting update listener and register the initial defaults static { CollectionSettingsRegistry registry = CollectionSettingsRegistry.getInstance(); registry.addListener(new CollectionSettingsUpdateListener() { @SuppressWarnings("synthetic-access") public void incrementalUpdate(CollectionSettingName name, Serializable value) { InsightLogger LOG = InsightLogManager.getLogger(AbstractFilesTrackerAspectSupport.class.getName()); if (MAX_TRACKED_FILES_SETTING.equals(name)) { int newCapacity = CollectionSettingsRegistry.getIntegerSettingValue(value); if (newCapacity <= 0) { throw new IllegalArgumentException("Negative capacity N/A: " + value); } int prevCapacity = filesCache.updateMaxCapacity(newCapacity); if (prevCapacity != newCapacity) { LOG.info("incrementalUpdate(" + name + ") " + prevCapacity + " => " + newCapacity); } } else if (MAPPINGS_TRACKER_LOG_SETTING.equals(name)) { Level newValue = CollectionSettingsRegistry.getLogLevelSetting(value); if (newValue != logLevel) { LOG.info("incrementalUpdate(" + name + ") " + logLevel + " => " + newValue); } logLevel = newValue; } } }); registry.register(MAX_TRACKED_FILES_SETTING, Integer.valueOf(DEFAULT_FILE_CACHE_SIZE)); registry.register(MAPPINGS_TRACKER_LOG_SETTING, DEFAULT_LEVEL); } protected AbstractFilesTrackerAspectSupport() { super(); } boolean collectExtraInformation() { return FrameBuilder.OperationCollectionLevel.HIGH.equals(configuration.getCollectionLevel()); } Operation registerOperation(Operation op) { if (op == null) { return op; } op.label(createOperationLabel(op)); /* * NOTE: we generate a zero-duration frame since the purpose of this * plugin is to track the files. Furthermore, actually measuring the * duration of the open/close calls seems too complex (at least for now) */ OperationCollector collector = getCollector(); collector.enter(op); collector.exitNormal(); return op; } Operation createOperation(JoinPoint.StaticPart staticPart, String action, String filePath) { return new Operation() .type(FilesTrackerDefinitions.TYPE) .sourceCodeLocation(OperationCollectionUtil.getSourceCodeLocation(staticPart)) .put(FilesTrackerDefinitions.OPTYPE_ATTR, action) .put(FilesTrackerDefinitions.PATH_ATTR, filePath) ; } Operation addExtraInformation(Operation op, File f) { if ((op == null) || (f == null)) { return op; } boolean exists = f.exists(); op.put("exists", exists); // files being written might not exist if (exists) { op.put("size", f.length()) .put("lastModified", f.lastModified()) .put("isFile", f.isFile()) .put("isDirectory", f.isDirectory()) .put("isAbsolute", f.isAbsolute()) .put("isHidden", f.isHidden()) .put("isReadable", f.canRead()) .put("isWriteable", f.canWrite()) // only for J2SE 1.6 .put("isExecutable", f.canExecute()) ; } return op; } /** * @param instance The {@link Closeable} instance created to access the file - * ignored if <code>null</code> * @param f The accessed {@link File} - ignored if <code>null</code> * @param mode The file access mode * @return The previously tracked file path by the accessor instance - * <code>null</code> if no such file (which is the normal expected value) */ protected String mapOpenedFile(Closeable instance, File f, String mode) { return mapOpenedFile(instance, (f == null) ? null : f.getAbsolutePath(), mode); } /** * @param instance The {@link Closeable} instance created to access the file - * ignored if <code>null</code> * @param filePath The accessed file path - ignored if <code>null</code>/empty * @param mode The file access mode * @return The previously tracked file path by the accessor instance - * <code>null</code> if no such file (which is the normal expected value) */ protected String mapOpenedFile(Closeable instance, String filePath, String mode) { if ((filePath == null) || (filePath.length() <= 0)) { return null; } CacheKey k = CacheKey.getFileKey(instance); if (k == null) { return null; } String prev = trackedFilesMap.put(k, filePath); if ((logLevel != null) && (!Level.OFF.equals(logLevel)) && _logger.isLoggable(logLevel)) { _logger.log(logLevel, "mapOpenedFile(" + filePath + ")[" + mode + "]@" + k + " => " + prev); } return prev; } /** * @param instance The {@link Closeable} instance being closed - ignored * if <code>null</code> * @return The file path being accessed by the instance - <code>null</code> * if unknown file */ protected String unmapClosedFile(Closeable instance) { CacheKey k = CacheKey.getFileKey(instance); if (k == null) { return null; } String filePath = trackedFilesMap.remove(k); if ((logLevel != null) && (!Level.OFF.equals(logLevel)) && _logger.isLoggable(logLevel)) { _logger.log(logLevel, "unmapClosedFile(" + k + "): " + filePath); } return filePath; } protected String createOperationLabel(Operation op) { if (op == null) { return null; } else { return createOperationLabel(op.get(FilesTrackerDefinitions.OPTYPE_ATTR, String.class), op.get(FilesTrackerDefinitions.PATH_ATTR, String.class)); } } static final String createOperationLabel(String action, String path) { String displayAction = StringUtil.capitalize(action); String displayPath = StringUtil.chopHeadAndEllipsify(path, StringFormatterUtils.MAX_PARAM_LENGTH); return displayAction + " " + displayPath; } @Override public String getPluginName() { return FilesTrackerPluginRuntimeDescriptor.PLUGIN_NAME; } /** * A rather simplistic LRU cache to ensure that if we miss the file close * of the tracked files map does not grow indefinitely. The map itself is * not synchronized but it is <U>wrapped</U> into one. */ static final class FilesCache extends LinkedHashMap<CacheKey, String> { private static final long serialVersionUID = 1034264756856652293L; private volatile int maxCapacity; public FilesCache(int maxCapacityValue) { super(maxCapacityValue); this.maxCapacity = maxCapacityValue; } public int getMaxCapacity() { return this.maxCapacity; } public int updateMaxCapacity(int maxCapacityValue) { int prev = this.maxCapacity; this.maxCapacity = maxCapacityValue; return prev; } @Override protected boolean removeEldestEntry(Entry<CacheKey, String> eldest) { return size() > maxCapacity; } } static final class CacheKey { private final String name; private final int value, hash; private CacheKey(Closeable instance) { name = instance.getClass().getName(); value = System.identityHashCode(instance); // we can calculate the hash now since all values are final hash = name.hashCode() + value; } @Override public int hashCode() { return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (getClass() != obj.getClass()) { return false; } CacheKey other = (CacheKey) obj; if (name.equals(other.name) && (value == other.value)) { return true; } return false; } @Override public String toString() { return name + "@" + Integer.toHexString(value); } static CacheKey getFileKey(Closeable instance) { if (instance == null) return null; else return new CacheKey(instance); } } @Override public boolean isMetricsGenerator() { return true; // This provides an external resource } }