/* * Copyright (c) 2007, 2012, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.max.ins; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import com.sun.max.collect.*; import com.sun.max.ins.gui.*; import com.sun.max.ins.util.*; import com.sun.max.program.*; import com.sun.max.program.option.*; import com.sun.max.vm.hosted.*; /** * A manager for saving and restoring of settings between Inspection sessions, with some * specialized machinery to support the saving of window frame geometry (location, size) * for both the Inspector's main frame and for views within it. */ public class InspectionSettings { private static final int TRACE_VALUE = 2; private static final String VM_SETTINGS_KEY = "vm"; private static final String VM_BOOT_IMAGE_FORMAT_VERSION_KEY = "bootImageFormatVersion"; private static final String VM_BOOT_IMAGE_ID_KEY = "bootImageId"; private static final String INSPECTOR_SETTINGS_KEY = "inspector"; private static final String INSPECTOR_SETTINGS_VERSION_KEY = "settingsVersion"; // This should be incremented any time that any of the persistent settings keys changes. private static final String INSPECTOR_SETTINGS_VERSION = "3"; private static final String COMPONENT_X_KEY = "x"; private static final String COMPONENT_Y_KEY = "y"; private static final String COMPONENT_HEIGHT_KEY = "height"; private static final String COMPONENT_WIDTH_KEY = "width"; private static final Point defaultInspectorLocation = new Point(100, 100); private String tracePrefix() { return "[InspectionSettings] "; } /** * @return a unique key uniquely identifying a key belonging to a client that stores and retrieves settings. */ private String makeClientKey(SaveSettingsListener saveSettingsListener, String key) { return saveSettingsListener.name() + "." + key; } /** * An event requesting that a {@link SaveSettingsListener} write its settings to a persistent store. The * event object includes a method for a listener to {@linkplain #save(String, Object) save} its settings. */ public final class SaveSettingsEvent { private final Properties props; private final SaveSettingsListener client; /** * Creates an object that will populate a given properties object with the settings of a given client. * * @param saveSettingsListener a settings listener that will save it settings via this object * @param properties the properties object into which the settings will be written */ private SaveSettingsEvent(SaveSettingsListener saveSettingsListener, Properties properties) { props = properties; client = saveSettingsListener; } /** * Saves a value for a setting corresponding to a given name. * * @param key the name of the setting * @param value the value of the setting. The value saved is the result of calling {@link String#valueOf(Object)} on this value. */ public void save(String key, Object value) { final String clientKey = makeClientKey(client, key); props.setProperty(clientKey, String.valueOf(value)); } } /** * An object that uses settings to configure itself and therefore wants to participate in the saving of settings to * a persistent store. */ public interface SaveSettingsListener { /** * Notifies this object that it should save it settings. * * @param saveSettingsEvent an object to which this client should write its persistent settings */ void saveSettings(SaveSettingsEvent saveSettingsEvent); /** * Gets a name for this client that is unique amongst all other clients. This name forms the * prefix of the key under which any persistent setting of this client is saved. */ String name(); /** * The view responsible for the displaying this object in a GUI. If this object does not have a visual * presentation, then this method should return null. * * In addition to the other listener specific settings, the {@linkplain Component#getBounds() geometry} of the * {@linkplain #view() view} responsible for the displaying the object are automatically saved * whenever the inspection settings are {@linkplain InspectionSettings#save() persisted}. The inspection * settings are persisted any time the inspection is * {@linkplain ComponentListener#componentMoved(ComponentEvent) moved} or * {@linkplain ComponentListener#componentResized(ComponentEvent) resized} and whenever the component is made * {@linkplain ComponentListener#componentShown(ComponentEvent) visible}, its bounds are updated from the * settings. */ InspectorView view(); /** * @return geometry to apply to a newly shown component when there have been no geometry settings saved. */ Rectangle defaultGeometry(); } /** * A convenience class that simplifies implementing the {@link SaveSettingsListener} interface. */ public abstract static class AbstractSaveSettingsListener implements SaveSettingsListener { protected final String name; protected final InspectorView view; protected final Rectangle defaultGeometry; private AbstractSaveSettingsListener(String name, InspectorView view, Rectangle defaultGeometry) { this.name = name; this.view = view; this.defaultGeometry = defaultGeometry; } public AbstractSaveSettingsListener(String name, InspectorView view) { this(name, view, null); } protected AbstractSaveSettingsListener(String name) { this (name, null, null); } public InspectorView view() { return view; } public String name() { return name; } public Rectangle defaultGeometry() { return defaultGeometry; } @Override public String toString() { return name(); } } private final Inspection inspection; private final Properties properties = new SortedProperties(); private final File settingsFile; private final boolean bootImageChanged; private final Map<String, SaveSettingsListener> clients; public InspectionSettings(Inspection inspection, File settingsFile) { this.inspection = inspection; this.settingsFile = settingsFile; clients = new IdentityHashMap<String, SaveSettingsListener>(); try { final FileReader fileReader = new FileReader(settingsFile); Trace.begin(1, tracePrefix() + "loading preferences from: " + settingsFile.toString()); properties.load(fileReader); Trace.end(1, tracePrefix() + "loading preferences from: " + settingsFile.toString()); fileReader.close(); } catch (FileNotFoundException ioException) { } catch (IOException ioException) { InspectorWarning.message(inspection, tracePrefix() + "Error while loading settings from " + settingsFile, ioException); } // Check that the settings keys haven't changed since the previous session; // if so warn and ignore the old settings. final SaveSettingsListener inspectorClient = new AbstractSaveSettingsListener(INSPECTOR_SETTINGS_KEY) { public void saveSettings(SaveSettingsEvent settings) { settings.save(INSPECTOR_SETTINGS_VERSION_KEY, String.valueOf(INSPECTOR_SETTINGS_VERSION)); } }; addSaveSettingsListener(inspectorClient); final int settingsVersion = get(inspectorClient, INSPECTOR_SETTINGS_VERSION_KEY, OptionTypes.INT_TYPE, 0); if (settingsVersion != Integer.valueOf(INSPECTOR_SETTINGS_VERSION)) { InspectorWarning.message(inspection, tracePrefix() + "Settings in obsolete format ignored"); properties.clear(); } // Check to see if the boot image being inspected is the same as the previous inspection session. final BootImage bootImage = inspection.vm().bootImage(); final SaveSettingsListener bootimageClient = new AbstractSaveSettingsListener(VM_SETTINGS_KEY) { public void saveSettings(SaveSettingsEvent settings) { settings.save(VM_BOOT_IMAGE_FORMAT_VERSION_KEY, String.valueOf(bootImage.header.bootImageFormatVersion)); settings.save(VM_BOOT_IMAGE_ID_KEY, String.valueOf(bootImage.header.randomID)); } }; addSaveSettingsListener(bootimageClient); final int version = get(bootimageClient, VM_BOOT_IMAGE_FORMAT_VERSION_KEY, OptionTypes.INT_TYPE, 0); final int randomID = get(bootimageClient, VM_BOOT_IMAGE_ID_KEY, OptionTypes.INT_TYPE, 0); bootImageChanged = version != bootImage.header.bootImageFormatVersion || randomID != bootImage.header.randomID; bootimageClient.saveSettings(new SaveSettingsEvent(bootimageClient, properties)); saver = new Saver(); } /** * Add a listener that will be notified when a "save event" is triggered, so that settings * can be saved. * <p> * If the listener has an associated {@link AbstractView}, then additional services are provided * to automate the saving and restoring of window frame geometry (size, location): * <ol> * <li>During the execution of this method call, the view's frame is positioned and * sized according to the following search: * <ul> * <li>Use saved geometry settings for this view, if they exist;</li> * <li>If no settings saved, then use default geometry for this kind of view, if it exists;</li> * <li>Otherwise use a generic default geometry.</li> * </ul> * </li> * <li>The listener has associated code that will automatically save the view's geometry * upon every "save event".</li> * </ol> * @param saveSettingsListener a listener for events that should cause important settings to be * saved */ public synchronized void addSaveSettingsListener(final SaveSettingsListener saveSettingsListener) { final SaveSettingsListener oldClient = clients.put(saveSettingsListener.name(), saveSettingsListener); assert oldClient == null || oldClient == saveSettingsListener; final InspectorView view = saveSettingsListener.view(); if (view != null) { view.getJComponent().addComponentListener(new ComponentListener() { public void componentHidden(ComponentEvent e) { } public void componentMoved(ComponentEvent e) { save(); } public void componentResized(ComponentEvent e) { save(); } public void componentShown(ComponentEvent e) { repositionInspectorFromSettings(saveSettingsListener); } }); repositionInspectorFromSettings(saveSettingsListener); } } public synchronized void removeSaveSettingsListener(final SaveSettingsListener saveSettingsListener) { clients.remove(saveSettingsListener.name()); } /** * Repositions a view's location from its previous settings, or * if none, from the default. Ensures that the ultimate location is visible. * * @param saveSettingsListener a listener that has an associated view */ private void repositionInspectorFromSettings(SaveSettingsListener saveSettingsListener) { final InspectorView view = saveSettingsListener.view(); final Rectangle oldGeometry = view.getJComponent().getBounds(); Rectangle newGeometry = saveSettingsListener.defaultGeometry(); // Check to see if we have geometry settings for this component. // We used to check to see if X location was set, with a default of -1 meaning // "not set", but we then discovered some apparently legitimate minus // values (for reasons unknown) on the Darwin platform (at least). if (get(saveSettingsListener, COMPONENT_WIDTH_KEY, OptionTypes.INT_TYPE, -1) >= 0) { newGeometry = new Rectangle( Math.max(get(saveSettingsListener, COMPONENT_X_KEY, OptionTypes.INT_TYPE, oldGeometry.x), 0), Math.max(get(saveSettingsListener, COMPONENT_Y_KEY, OptionTypes.INT_TYPE, oldGeometry.y), 0), get(saveSettingsListener, COMPONENT_WIDTH_KEY, OptionTypes.INT_TYPE, oldGeometry.width), get(saveSettingsListener, COMPONENT_HEIGHT_KEY, OptionTypes.INT_TYPE, oldGeometry.height)); } if (newGeometry == null) { view.getJComponent().setLocation(defaultInspectorLocation); } else if (!newGeometry.equals(oldGeometry)) { view.getJComponent().setBounds(newGeometry); } inspection.gui().moveToFullyVisible(view); } /** * Determines if the boot image identified in the file from which these settings were loaded * is different from the boot image of the current VM. */ public boolean bootImageChanged() { return bootImageChanged; } /** * Gets the value for a setting corresponding to a given name. * * @param key the name of the setting for which the value is required * @param type an object that encodes the (boxed) type of the value as well as the capability for parsing a string * into a value of the type * @param defaultValue the value to return if there is not value in this object corresponding to {@code key} * @throws Option.Error if there is an error parsing the string representation of the value into a value of the * expected type */ public <Value_Type> Value_Type get(SaveSettingsListener saveSettingsListener, String key, Option.Type<Value_Type> type, Value_Type defaultValue) { if (!clients.containsKey(saveSettingsListener.name())) { throw new IllegalArgumentException("Unregistered settings client: " + saveSettingsListener.name()); } final String clientKey = makeClientKey(saveSettingsListener, key); final String value = properties.getProperty(clientKey); if (value == null) { return defaultValue; } try { return type.parseValue(value); } catch (Option.Error optionError) { throw new Option.Error(String.format("Problem occurred while parsing %s for %s setting:%n%s", clientKey, value, optionError.getMessage())); } } private boolean needsSaving; /** * A helper task that is run in a separate thread for writing the persistent settings to a file. * Performing this task asynchronously means that a number of changes can be batched to the * file. */ private class Saver implements Runnable { public void run() { while (!done) { try { Thread.sleep(1000); } catch (InterruptedException e) { } if (!done && needsSaving) { doSave(); needsSaving = false; } } } boolean done; void quit() { done = true; doSave(); } Saver() { new Thread(this, "SettingsSaver").start(); } } private final Saver saver; /** * Determines if there is a setting in this object named by a given key. */ public boolean containsKey(SaveSettingsListener saveSettingsListener, String key) { final String clientKey = makeClientKey(saveSettingsListener, key); return properties.containsKey(clientKey); } public synchronized void quit() { saver.quit(); } /** * Indicates that the persistent settings represented by this object have changed * and should be saved to a {@linkplain #settingsFile file}. The actual writing * to the file happens asynchronously. */ public void save() { needsSaving = true; } /** * Writes the persistent settings represented by this object to a {@linkplain #settingsFile file}. */ private synchronized void doSave() { updateSettings(); try { final FileWriter fileWriter = new FileWriter(settingsFile); properties.store(fileWriter, null); fileWriter.close(); } catch (IOException ioException) { InspectorWarning.message(inspection, tracePrefix() + "Error while saving settings to " + settingsFile, ioException); } } private void updateSettings() { final Properties newProperties = new SortedProperties(); Trace.line(TRACE_VALUE, tracePrefix() + "saving settings to: " + settingsFile.toString()); for (SaveSettingsListener saveSettingsListener : clients.values()) { final SaveSettingsEvent saveSettingsEvent = new SaveSettingsEvent(saveSettingsListener, newProperties); saveSettingsListener.saveSettings(saveSettingsEvent); final InspectorView view = saveSettingsListener.view(); if (view != null) { final Rectangle geometry = view.getJComponent().getBounds(); saveSettingsEvent.save(COMPONENT_X_KEY, geometry.x); saveSettingsEvent.save(COMPONENT_Y_KEY, geometry.y); saveSettingsEvent.save(COMPONENT_WIDTH_KEY, geometry.width); saveSettingsEvent.save(COMPONENT_HEIGHT_KEY, geometry.height); Trace.line(TRACE_VALUE, tracePrefix() + "saving locn=(" + geometry.x + "," + geometry.y + "),size=(" + geometry.width + "," + geometry.height + ") for " + view.getClass().getSimpleName()); } } properties.putAll(newProperties); } /** * Writes a summary, in alphabetical order, of the current settings being saved. */ public void writeSummary(PrintStream printStream) { updateSettings(); try { properties.store(printStream, "Inspection settings"); } catch (IOException e) { InspectorWarning.message(inspection, "Failed to write settings to console", e); } } }