Merge remote-tracking branch 'origin/GP-5992-dragonmacher-options-age-off--SQUASHED'

This commit is contained in:
Ryan Kurtz 2025-09-26 13:01:48 -04:00
commit e8ba7c3e45
10 changed files with 568 additions and 127 deletions

View file

@ -24,6 +24,8 @@ import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditorSupport;
import java.io.File;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.*;
import javax.swing.JComponent;
@ -34,7 +36,7 @@ import org.junit.*;
import generic.test.AbstractGuiTest;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.util.HelpLocation;
import ghidra.util.*;
import ghidra.util.bean.opteditor.OptionsVetoException;
import ghidra.util.exception.InvalidInputException;
import gui.event.MouseBinding;
@ -454,12 +456,133 @@ public class OptionsTest extends AbstractGuiTest {
assertTrue(options.contains("Foo"));
assertTrue(options.contains("Bar"));
options.registerOption("Bar", 0, null, "foo");
options.registerOption("Bar", 0, null, "Bar description");
options.removeUnusedOptions();
assertFalse(options.contains("Foo"));
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, but not aged-off; should stay around
assertTrue(options.contains("Foo"));
// Save, edit the date of the options to be older than 1 year, which is our age cutoff.
// Make sure any unregistered option is then removed.
Element savedRoot = options.getXmlRoot(false);
LocalDate twoYearsAgo = LocalDate.now().minusYears(2);
Set<String> optionNames = Set.of("Bar", "Foo");
setLastUsedDate(savedRoot, twoYearsAgo, optionNames);
options = new ToolOptions(savedRoot);
assertTrue(options.contains("Foo"));
assertTrue(options.contains("Bar"));
options.registerOption("Bar", 0, null, "Bar description");
options.removeUnusedOptions();
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, and older than 1 year; should be removed
assertFalse(options.contains("Foo"));
}
@Test
public void testRemoveUnusedOption_WrappedOption() throws Exception {
Date date = new Date();
options.setDate("Foo", date);
options.setDate("Bar", date);
saveAndRestoreOptions();
assertEquals(date, options.getDate("Foo", null));
assertEquals(date, options.getDate("Bar", null));
Date defaultDate = new SimpleDateFormat("yyyy-MM-dd").parse("2025-09-12");
options.registerOption("Bar", defaultDate, null, "Bar description");
options.removeUnusedOptions();
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, but not aged-off; should stay around
assertTrue(options.contains("Foo"));
// Save, edit the date of the options to be older than 1 year, which is our age cutoff.
// Make sure any unregistered option is then removed.
Element savedRoot = options.getXmlRoot(false);
LocalDate twoYearsAgo = LocalDate.now().minusYears(2);
Set<String> optionNames = Set.of("Bar", "Foo");
setLastUsedDate(savedRoot, twoYearsAgo, optionNames);
options = new ToolOptions(savedRoot);
assertTrue(options.contains("Foo"));
assertTrue(options.contains("Bar"));
options.registerOption("Bar", defaultDate, null, "Bar description");
options.removeUnusedOptions();
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, and older than 1 year; should be removed
assertFalse(options.contains("Foo"));
}
@Test
public void testUnregisteredWarningMessage() {
/*
Test that options access, but not registered, will trigger a warning message.
*/
SpyErrorLogger spyLogger = new SpyErrorLogger();
Msg.setErrorLogger(spyLogger);
options.getInt("Foo", 1);
assertTrue(spyLogger.isEmtpy());
options.validateOptions();
spyLogger.assertLogMessage("Unregistered", "property", "Foo");
spyLogger.reset();
options.registerOption("Foo", 0, null, "Description");
options.getInt("Foo", 2);
options.validateOptions();
assertTrue(spyLogger.isEmtpy());
}
private void setLastUsedDate(Element savedRoot, LocalDate lastRegisteredDate,
Set<String> names) {
/*
Msg.debug(this, XmlUtilities.toString(savedRoot));
<CATEGORY NAME="Test">
<SAVE_STATE NAME="Bar" TYPE="int" VALUE="3" LAST_REGISTERED="2025-09-17" />
<SAVE_STATE NAME="Bar" TYPE="boolean" VALUE="false" LAST_REGISTERED="2025-09-17" />
</CATEGORY>
*/
String dateString = lastRegisteredDate.format(ToolOptions.LAST_REGISTERED_DATE_FORMATTER);
List<Element> elements = getElementsByName(savedRoot, names);
for (Element element : elements) { // <SAVE_STATE NAME="Foo" ...>
element.setAttribute(ToolOptions.LAST_REGISTERED_DATE_ATTIBUTE, dateString);
}
}
@SuppressWarnings("unchecked")
private List<Element> getElementsByName(Element savedRoot, Set<String> names) {
List<Element> matches = new ArrayList<>();
List<Element> children = savedRoot.getChildren();
for (Element saveState : children) {
String name = saveState.getAttributeValue("NAME");
if (names.contains(name)) {
matches.add(saveState);
}
}
return matches;
}
@Test

View file

@ -538,7 +538,7 @@ public class DecompileOptions {
grabFromProgram(program);
// assuming if one is not registered, then none area
// assuming if one is not registered, then none are
if (!opt.isRegistered(PREDICATE_OPTIONSTRING)) {
return;
}

View file

@ -0,0 +1,154 @@
/* ###
* IP: GHIDRA
*
* 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 ghidra.framework.options;
import java.util.*;
import java.util.Map.Entry;
import org.jdom.Attribute;
import org.jdom.Element;
/**
* A version of {@link SaveState} that allows clients to add attributes to properties in this save
* state. The following code shows how to use this class:
* <pre>
* AttributedSaveState ss = new AttributedSaveState();
* ss.putBoolean("Happy", true);
*
* Map<String, String> attrs = Map.of("MyAttribute", "AttributeValue");
* ss.addAttrbibutes("Happy", attrs);
* </pre>
*
* <p>In this example, the property "Happy" will be given the attribute "MyAttribute" with the value
* of "AttributeValue". This is useful for clients that wish to add attributes to individual
* properties, such as a date for tracking usage.
*
* <p><u>Usage Note:</u> The given attributes are only supported when writing and reading xml. Json
* is not supported.
*/
public class AttributedSaveState extends SaveState {
private Map<String, Map<String, String>> propertyAttributes;
public AttributedSaveState() {
super();
}
public AttributedSaveState(String name) {
super(name);
}
public AttributedSaveState(Element root) {
super(root);
}
/**
* Adds the given map of attribute name/value pairs to this save state.
* @param propertyName the property name within this save state that will be attributed
* @param attributes the attributes
*/
public void addAttributes(String propertyName, Map<String, String> attributes) {
getPropertyAttributes().put(propertyName, attributes);
}
/**
* Removes all attributes associated with the given property name.
* @param propertyName the property name within this save state that has the given attributes
*/
public void removeAttributes(String propertyName) {
getPropertyAttributes().remove(propertyName);
}
/**
* Gets the attributes currently associated with the given property name
* @param propertyName the property name for which to get attributes
* @return the attributes or null
*/
public Map<String, String> getAttributes(String propertyName) {
return getPropertyAttributes().get(propertyName);
}
private Map<String, Map<String, String>> getPropertyAttributes() {
if (propertyAttributes == null) {
propertyAttributes = new HashMap<>();
}
return propertyAttributes;
}
@Override
protected SaveState createSaveState() {
return new AttributedSaveState();
}
@Override
protected void initializeElement(Element e) {
String name = e.getAttributeValue(NAME);
if (name == null) {
return; // sub-element; properties not supported
}
//
// Overridden to add our attributes to the newly created element, used to create xml
//
Map<String, String> attrs = getPropertyAttributes().get(name);
if (attrs != null) {
Set<Entry<String, String>> entries = attrs.entrySet();
for (Entry<String, String> entry : entries) {
String key = entry.getKey();
String value = entry.getValue();
e.setAttribute(key, value);
}
}
}
@Override
protected void processElement(Element element) {
super.processElement(element);
String name = element.getAttributeValue(NAME);
if (name == null) {
return; // sub-element; properties not supported
}
//
// Overridden to extract non-standard attributes from the given element. The element was
// created after restoring from xml. We extract the attributes that we added above in
// initializeElement().
//
Map<String, String> newAttrs = new HashMap<>();
@SuppressWarnings("unchecked")
List<Attribute> attrs = element.getAttributes();
for (Attribute attr : attrs) {
String attrName = attr.getName();
String attrValue = switch (attrName) {
// ignore standard attributes, as they are managed by the parent class
case NAME, TYPE, VALUE -> null;
default -> attr.getValue();
};
if (attrValue != null) {
newAttrs.put(attrName, attrValue);
}
}
if (!newAttrs.isEmpty()) {
getPropertyAttributes().put(name, newAttrs);
}
}
}

View file

@ -78,7 +78,7 @@ public class GProperties {
private static final String STATE = "STATE";
protected static final String TYPE = "TYPE";
protected static final String NAME = "NAME";
private static final String VALUE = "VALUE";
protected static final String VALUE = "VALUE";
public static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
private static final String ARRAY_ELEMENT_NAME = "A";
protected TreeMap<String, Object> map; // use ordered map for deterministic serialization
@ -505,35 +505,34 @@ public class GProperties {
return root;
}
protected Element createElement(String key, Object value) {
protected Element createElement(String propertyName, Object value) {
Element elem = null;
if (value instanceof Element) {
elem = createElementFromElement(key, (Element) value);
elem = createElementFromElement(propertyName, (Element) value);
}
else if (value instanceof Byte) {
elem = setAttributes(key, "byte", ((Byte) value).toString());
elem = setAttributes(propertyName, "byte", ((Byte) value).toString());
}
else if (value instanceof Short) {
elem = setAttributes(key, "short", ((Short) value).toString());
elem = setAttributes(propertyName, "short", ((Short) value).toString());
}
else if (value instanceof Integer) {
elem = setAttributes(key, "int", ((Integer) value).toString());
elem = setAttributes(propertyName, "int", ((Integer) value).toString());
}
else if (value instanceof Long) {
elem = setAttributes(key, "long", ((Long) value).toString());
elem = setAttributes(propertyName, "long", ((Long) value).toString());
}
else if (value instanceof Float) {
elem = setAttributes(key, "float", ((Float) value).toString());
elem = setAttributes(propertyName, "float", ((Float) value).toString());
}
else if (value instanceof Double) {
elem = setAttributes(key, "double", ((Double) value).toString());
elem = setAttributes(propertyName, "double", ((Double) value).toString());
}
else if (value instanceof Boolean) {
elem = setAttributes(key, "boolean", ((Boolean) value).toString());
elem = setAttributes(propertyName, "boolean", ((Boolean) value).toString());
}
else if (value instanceof String) {
elem = new Element(STATE);
elem.setAttribute(NAME, key);
elem = createElement(STATE, propertyName);
elem.setAttribute(TYPE, "string");
if (XmlUtilities.hasInvalidXMLCharacters((String) value)) {
elem.setAttribute("ENCODED_VALUE", NumericUtilities
@ -544,71 +543,66 @@ public class GProperties {
}
}
else if (value instanceof Color) {
elem = setAttributes(key, "Color", Integer.toString(((Color) value).getRGB()));
elem = setAttributes(propertyName, "Color", Integer.toString(((Color) value).getRGB()));
}
else if (value instanceof Date) {
elem = setAttributes(key, "Date", DATE_FORMAT.format((Date) value));
elem = setAttributes(propertyName, "Date", DATE_FORMAT.format((Date) value));
}
else if (value instanceof File) {
elem = setAttributes(key, "File", ((File) value).getAbsolutePath());
elem = setAttributes(propertyName, "File", ((File) value).getAbsolutePath());
}
else if (value instanceof KeyStroke) {
elem = setAttributes(key, "KeyStroke", value.toString());
elem = setAttributes(propertyName, "KeyStroke", value.toString());
}
else if (value instanceof Font font) {
elem = setAttributes(key, "Font", toFontString(font));
elem = setAttributes(propertyName, "Font", toFontString(font));
}
else if (value instanceof byte[]) {
elem = new Element("BYTES");
elem.setAttribute(NAME, key);
elem = createElement("BYTES", propertyName);
elem.setAttribute(VALUE, NumericUtilities.convertBytesToString((byte[]) value));
}
else if (value instanceof short[]) {
elem = setArrayAttributes(key, "short", value);
elem = setArrayAttributes(propertyName, "short", value);
}
else if (value instanceof int[]) {
elem = setArrayAttributes(key, "int", value);
elem = setArrayAttributes(propertyName, "int", value);
}
else if (value instanceof long[]) {
elem = setArrayAttributes(key, "long", value);
elem = setArrayAttributes(propertyName, "long", value);
}
else if (value instanceof float[]) {
elem = setArrayAttributes(key, "float", value);
elem = setArrayAttributes(propertyName, "float", value);
}
else if (value instanceof double[]) {
elem = setArrayAttributes(key, "double", value);
elem = setArrayAttributes(propertyName, "double", value);
}
else if (value instanceof boolean[]) {
elem = setArrayAttributes(key, "boolean", value);
elem = setArrayAttributes(propertyName, "boolean", value);
}
else if (value instanceof String[]) {
elem = setArrayAttributes(key, "string", value);
elem = setArrayAttributes(propertyName, "string", value);
}
else if (value instanceof Enum) {
Enum<?> e = (Enum<?>) value;
elem = new Element("ENUM");
elem.setAttribute(NAME, key);
elem = createElement("ENUM", propertyName);
elem.setAttribute(TYPE, "enum");
elem.setAttribute("CLASS", e.getClass().getName());
elem.setAttribute(VALUE, e.name());
}
else if (value instanceof GProperties) {
Element savedElement = ((GProperties) value).saveToXml();
elem = new Element(GPROPERTIES_TAG);
elem.setAttribute(NAME, key);
elem = createElement(GPROPERTIES_TAG, propertyName);
elem.setAttribute(TYPE, G_PROPERTIES_TYPE);
elem.addContent(savedElement);
}
else {
elem = new Element("NULL");
elem.setAttribute(NAME, key);
elem = createElement("NULL", propertyName);
}
return elem;
}
private <T> Element setArrayAttributes(String key, String type, Object values) {
Element elem = new Element("ARRAY");
elem.setAttribute(NAME, key);
private <T> Element setArrayAttributes(String propertyName, String type, Object values) {
Element elem = createElement("ARRAY", propertyName);
elem.setAttribute(TYPE, type);
for (int i = 0; i < Array.getLength(values); i++) {
Object value = Array.get(values, i);
@ -621,10 +615,19 @@ public class GProperties {
return elem;
}
private Element setAttributes(String key, String type, String value) {
Element elem;
elem = new Element(STATE);
elem.setAttribute(NAME, key);
protected Element createElement(String tag, String name) {
Element e = new Element(tag);
e.setAttribute(NAME, name);
initializeElement(e);
return e;
}
protected void initializeElement(Element e) {
// subclasses may override
}
private Element setAttributes(String propertyName, String type, String value) {
Element elem = createElement(STATE, propertyName);
elem.setAttribute(TYPE, type);
elem.setAttribute(VALUE, value);
return elem;
@ -792,8 +795,7 @@ public class GProperties {
}
protected Element createElementFromElement(String internalKey, Element internalElement) {
Element newElement = new Element("XML");
newElement.setAttribute(NAME, internalKey);
Element newElement = createElement("XML", internalKey);
Element internalElementClone = (Element) internalElement.clone();
newElement.addContent(internalElementClone);
@ -1420,11 +1422,11 @@ public class GProperties {
return true;
}
private String toFontString(Font font) {
private static String toFontString(Font font) {
return String.format("%s-%s-%s", font.getName(), getStyleString(font), font.getSize());
}
private String getStyleString(Font font) {
private static String getStyleString(Font font) {
boolean bold = font.isBold();
boolean italic = font.isItalic();
if (bold && italic) {

View file

@ -17,6 +17,7 @@ package ghidra.framework.options;
import java.io.File;
import java.io.IOException;
import java.util.List;
import org.jdom.Element;
@ -114,31 +115,86 @@ public class SaveState extends XmlProperties {
return getAsType(name, null, SaveState.class);
}
@SuppressWarnings("unchecked")
@Override
protected void processElement(Element element) {
String tag = element.getName();
if (tag.equals("SAVE_STATE")) {
Element child = (Element) element.getChildren().get(0);
if (child != null) {
String name = element.getAttributeValue(NAME);
map.put(name, new SaveState(child));
return;
}
if (!tag.equals("SAVE_STATE")) {
super.processElement(element);
return;
}
super.processElement(element);
/*
When using a SaveState inside of a SaveState, we produce xml that looks like this:
<SAVE_STATE NAME="Bar" TYPE="SaveState">
<STATE NAME="Bar" TYPE="int" VALUE="3" />
</SAVE_STATE>
*/
SaveState saveState = createSaveState();
List<Element> children = element.getChildren();
if (children.isEmpty()) {
return;
}
Element child = (Element) element.getChildren().get(0);
String childTag = child.getName();
if (childTag.equals("SAVE_STATE")) {
/*
Old style tag, with one level of extra nesting
<SAVE_STATE NAME="Bar" TYPE="SaveState">
<SAVE_STATE>
<STATE NAME="DATED_OPTION" TYPE="int" VALUE="3" />
</SAVE_STATE>
</SAVE_STATE>
*/
children = child.getChildren();
}
for (Element e : children) {
saveState.processElement(e);
}
String parentName = element.getAttributeValue(NAME);
map.put(parentName, saveState);
}
@SuppressWarnings("unchecked")
@Override
protected Element createElement(String key, Object value) {
if (value instanceof SaveState saveState) {
Element savedElement = saveState.saveToXml();
Element element = new Element("SAVE_STATE");
element.setAttribute(NAME, key);
element.setAttribute(TYPE, "SaveState");
element.addContent(savedElement);
return element;
if (!(value instanceof SaveState saveState)) {
return super.createElement(key, value);
}
return super.createElement(key, value);
/*
When using a SaveState inside of a SaveState, we produce xml that looks like this:
<SAVE_STATE NAME="Bar" TYPE="SaveState">
<STATE NAME="Bar" TYPE="int" VALUE="3" />
</SAVE_STATE>
*/
Element savedElement = saveState.saveToXml();
Element element = new Element("SAVE_STATE");
element.setAttribute(NAME, key);
element.setAttribute(TYPE, "SaveState");
// do not write an extra <SAVE_STATE> intermediate node
List<Element> children = savedElement.getChildren();
for (Element e : children) {
Element newElement = (Element) e.clone();
element.addContent(newElement);
}
return element;
}
// allows subclasses to override how sub-save states are created
protected SaveState createSaveState() {
return new SaveState();
}
}

View file

@ -4,9 +4,9 @@
* 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.
@ -15,7 +15,7 @@
*/
package ghidra.util;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.Iterator;
@ -88,6 +88,10 @@ public class SpyErrorLogger implements ErrorLogger, Iterable<String> {
messages.clear();
}
public boolean isEmtpy() {
return messages.isEmpty();
}
public void assertLogMessage(String... words) {
for (String message : this) {
if (StringUtilities.containsAllIgnoreCase(message, words)) {

View file

@ -260,8 +260,11 @@ public abstract class AbstractOptions implements Options {
if (option == null) {
return null;
}
if (option.getOptionType() != type) {
Msg.error(this, "Registered option incompatible with existing option: " + optionName,
OptionType existingType = option.getOptionType();
if (existingType != type) {
Msg.error(this, "Registered option incompatible with existing option: '%s'. " +
"Existing type '%s'; registered type '%s'.".formatted(optionName, existingType,
type),
new AssertException());
return null;
}

View file

@ -16,6 +16,7 @@
package ghidra.framework.options;
import java.beans.PropertyEditor;
import java.time.LocalDate;
import java.util.Objects;
import ghidra.util.HelpLocation;
@ -23,11 +24,15 @@ import ghidra.util.SystemUtilities;
import utilities.util.reflection.ReflectionUtilities;
public abstract class Option {
public static final String UNREGISTERED_OPTION = "Unregistered Option";
private static final LocalDate ONE_YEAR_AGO = LocalDate.now().minusYears(1);
private final String name;
private Object defaultValue;
private boolean isRegistered;
private LocalDate lastRegisteredDate;
private String description;
private HelpLocation helpLocation;
private OptionType optionType;
@ -45,7 +50,10 @@ public abstract class Option {
this.defaultValue = defaultValue;
this.isRegistered = isRegistered;
this.propertyEditor = editor;
if (!isRegistered) {
if (isRegistered) {
lastRegisteredDate = LocalDate.now();
}
else {
recordInception();
}
}
@ -67,6 +75,7 @@ public abstract class Option {
defaultValue = defaultValue != null ? defaultValue : updatedDefaultValue;
propertyEditor = propertyEditor != null ? propertyEditor : updatedEditor;
isRegistered = true;
lastRegisteredDate = LocalDate.now();
}
public abstract Object getCurrentValue();
@ -74,7 +83,8 @@ public abstract class Option {
public abstract void doSetCurrentValue(Object value);
public void setCurrentValue(Object value) {
this.isRegistered = true;
isRegistered = true;
lastRegisteredDate = LocalDate.now();
doSetCurrentValue(value);
}
@ -109,10 +119,44 @@ public abstract class Option {
return value;
}
public boolean wasRegisteredInPreviousSession() {
return lastRegisteredDate != null;
}
public boolean isRegistered() {
return isRegistered;
}
public void setLastRegisteredDate(LocalDate date) {
lastRegisteredDate = date;
}
public LocalDate getLastRegisteredDate() {
LocalDate date = lastRegisteredDate;
if (date == null) {
// This implies a new option that has not been registered and was not in a saved tool.
// Pick an old date so the option will not linger.
date = ONE_YEAR_AGO;
}
return date;
}
/**
* Returns true if the last registered date for this option is older than 1 year ago. That
* means that the option has not been registered and is likely no longer valid. This may not be
* true, if the given option still exists, but is only active on-demand by the user. If the
* option has expired, it will be removed. If it is still an existing on-demand option, it can
* again be saved when the user loads the owning provider and changes the option. In this case,
* it will remain in the tool for at least another year.
* @return true if expired
*/
public boolean hasExpired() {
if (lastRegisteredDate == null) {
return false;
}
return lastRegisteredDate.isBefore(ONE_YEAR_AGO);
}
public void restoreDefault() {
setCurrentValue(defaultValue);
}
@ -151,4 +195,5 @@ public abstract class Option {
public OptionType getOptionType() {
return optionType;
}
}

View file

@ -20,6 +20,8 @@ import java.awt.Font;
import java.beans.PropertyEditor;
import java.io.File;
import java.lang.reflect.Constructor;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import javax.swing.KeyStroke;
@ -45,10 +47,16 @@ import ghidra.util.exception.AssertException;
* <p>The Options Dialog shows the delimited hierarchy in tree format.
*/
public class ToolOptions extends AbstractOptions {
private static final String CLASS_ATTRIBUTE = "CLASS";
private static final String NAME_ATTRIBUTE = "NAME";
private static final String WRAPPED_OPTION_NAME = "WRAPPED_OPTION";
private static final String CLEARED_VALUE_ELEMENT_NAME = "CLEARED_VALUE";
public static final String LAST_REGISTERED_DATE_ATTIBUTE = "LAST_REGISTERED";
public static final DateTimeFormatter LAST_REGISTERED_DATE_FORMATTER =
DateTimeFormatter.ISO_LOCAL_DATE;
public static final Set<Class<?>> PRIMITIVE_CLASSES = buildPrimitiveClassSet();
public static final Set<Class<?>> WRAPPABLE_CLASSES = buildWrappableClassSet();
@ -91,7 +99,7 @@ public class ToolOptions extends AbstractOptions {
public ToolOptions(Element root) {
this(root.getAttributeValue(NAME_ATTRIBUTE));
SaveState saveState = new SaveState(root);
AttributedSaveState saveState = new AttributedSaveState(root);
readNonWrappedOptions(saveState);
@ -103,12 +111,23 @@ public class ToolOptions extends AbstractOptions {
}
}
private void readNonWrappedOptions(SaveState saveState) {
private void readNonWrappedOptions(AttributedSaveState saveState) {
for (String optionName : saveState.getNames()) {
Object object = saveState.getObject(optionName);
Option option =
createUnregisteredOption(optionName, OptionType.getOptionType(object), null);
// Get the last registered date. No date implies an old xml format.
LocalDate date = LocalDate.now();
Map<String, String> attrs = saveState.getAttributes(optionName);
String dateString = attrs.get(LAST_REGISTERED_DATE_ATTIBUTE);
if (dateString != null) {
date = LocalDate.parse(dateString, LAST_REGISTERED_DATE_FORMATTER);
}
OptionType type = OptionType.getOptionType(object);
Option option = createUnregisteredOption(optionName, type, null);
option.doSetCurrentValue(object); // use doSet versus set so that it is not registered
option.setLastRegisteredDate(date);
valueMap.put(optionName, option);
}
}
@ -124,19 +143,19 @@ public class ToolOptions extends AbstractOptions {
continue; // shouldn't happen
}
String optionName = element.getAttributeValue(NAME_ATTRIBUTE);
Class<?> c = Class.forName(element.getAttributeValue(CLASS_ATTRIBUTE));
Constructor<?> constructor = c.getDeclaredConstructor();
WrappedOption wo = (WrappedOption) constructor.newInstance();
wo.readState(new SaveState(element));
if (wo instanceof WrappedCustomOption wrappedCustom && !wrappedCustom.isValid()) {
continue;
}
if (wo instanceof WrappedKeyStroke wrappedKs) {
wo = wrappedKs.toWrappedActionTrigger();
}
String optionName = element.getAttributeValue(NAME_ATTRIBUTE);
Option option = createUnregisteredOption(optionName, wo.getOptionType(), null);
valueMap.put(optionName, option);
@ -149,6 +168,15 @@ public class ToolOptions extends AbstractOptions {
else {
option.doSetCurrentValue(wo.getObject()); // use doSet so that it is not registered
}
// Get the last registered date. No date implies an old xml format.
LocalDate date = LocalDate.now();
String dateString = element.getAttributeValue(LAST_REGISTERED_DATE_ATTIBUTE);
if (dateString != null) {
date = LocalDate.parse(dateString, LAST_REGISTERED_DATE_FORMATTER);
}
option.setLastRegisteredDate(date);
}
}
@ -162,7 +190,7 @@ public class ToolOptions extends AbstractOptions {
*/
public Element getXmlRoot(boolean includeDefaultBindings) {
SaveState saveState = new SaveState(XML_ELEMENT_NAME);
AttributedSaveState saveState = new AttributedSaveState(XML_ELEMENT_NAME);
writeNonWrappedOptions(includeDefaultBindings, saveState);
@ -174,18 +202,31 @@ public class ToolOptions extends AbstractOptions {
return root;
}
private void writeNonWrappedOptions(boolean includeDefaultBindings, SaveState saveState) {
private void writeNonWrappedOptions(boolean includeDefaultBindings,
AttributedSaveState saveState) {
for (String optionName : valueMap.keySet()) {
Option optionValue = valueMap.get(optionName);
if (includeDefaultBindings || !optionValue.isDefault()) {
Object value = optionValue.getValue(null);
if (isSupportedBySaveState(value)) {
saveState.putObject(optionName, value);
}
Option option = valueMap.get(optionName);
if (includeDefaultBindings || !option.isDefault()) {
writeNonWrappedOption(saveState, optionName, option);
}
}
}
private void writeNonWrappedOption(AttributedSaveState saveState, String optionName,
Option option) {
Object value = option.getValue(null);
if (!isSupportedBySaveState(value)) {
return;
}
saveState.putObject(optionName, value);
LocalDate date = option.getLastRegisteredDate();
String dateString = date.format(LAST_REGISTERED_DATE_FORMATTER);
Map<String, String> attrs = Map.of(LAST_REGISTERED_DATE_ATTIBUTE, dateString);
saveState.addAttributes(optionName, attrs);
}
private void writeWrappedOptions(boolean includeDefaultBindings, Element root) {
for (String optionName : valueMap.keySet()) {
Option option = valueMap.get(optionName);
@ -195,36 +236,47 @@ public class ToolOptions extends AbstractOptions {
}
if (includeDefaultBindings || !option.isDefault()) {
Object value = option.getCurrentValue();
if (isSupportedBySaveState(value)) {
continue; // handled above
}
WrappedOption wrappedOption = wrapOption(option);
if (wrappedOption == null) {
continue; // cannot write an option without a value to determine its type
}
SaveState ss = new SaveState(WRAPPED_OPTION_NAME);
Element elem = null;
if (value == null) {
// Handle the null case ourselves, not using the wrapped option (and when
// reading from xml) so the logic does not need to be in each wrapped option
elem = ss.saveToXml();
elem.addContent(new Element(CLEARED_VALUE_ELEMENT_NAME));
}
else {
wrappedOption.writeState(ss);
elem = ss.saveToXml();
}
elem.setAttribute(NAME_ATTRIBUTE, optionName);
elem.setAttribute(CLASS_ATTRIBUTE, wrappedOption.getClass().getName());
root.addContent(elem);
writeWrappedOption(root, optionName, option);
}
}
}
private void writeWrappedOption(Element root, String optionName, Option option) {
Object value = option.getCurrentValue();
if (isSupportedBySaveState(value)) {
return; // handled above
}
WrappedOption wrappedOption = wrapOption(option);
if (wrappedOption == null) {
return; // cannot write an option without a value to determine its type
}
SaveState ss = new SaveState(WRAPPED_OPTION_NAME);
Element element = null;
if (value == null) {
// Handle the null case ourselves, not using the wrapped option (and when
// reading from xml) so the logic does not need to be in each wrapped option
element = ss.saveToXml();
element.addContent(new Element(CLEARED_VALUE_ELEMENT_NAME));
}
else {
wrappedOption.writeState(ss);
element = ss.saveToXml();
}
element.setAttribute(NAME_ATTRIBUTE, optionName);
String className = wrappedOption.getClass().getName();
element.setAttribute(CLASS_ATTRIBUTE, className);
LocalDate date = option.getLastRegisteredDate();
String dateString = date.format(LAST_REGISTERED_DATE_FORMATTER);
element.setAttribute(LAST_REGISTERED_DATE_ATTIBUTE, dateString);
root.addContent(element);
}
private boolean isSupportedBySaveState(Object obj) {
if (obj == null) {
return false;
@ -310,7 +362,7 @@ public class ToolOptions extends AbstractOptions {
List<String> optionNames = new ArrayList<>(valueMap.keySet());
for (String optionName : optionNames) {
Option optionState = valueMap.get(optionName);
if (!optionState.isRegistered()) {
if (optionState.hasExpired()) {
removeOption(optionName);
}
}
@ -377,11 +429,15 @@ public class ToolOptions extends AbstractOptions {
Set<String> keySet = valueMap.keySet();
for (String propertyName : keySet) {
Option optionState = valueMap.get(propertyName);
if (optionState.isRegistered()) {
if (optionState.isRegistered() || optionState.wasRegisteredInPreviousSession()) {
continue;
}
Msg.warn(this, "Unregistered property \"" + propertyName + "\" in Options \"" + name +
"\"\n " + optionState.getInceptionInformation());
// getting here means that this option was used in the current tool session, but was not
// registered this session or in a previous session
Msg.warn(this,
"Unregistered property \"" + propertyName + "\" in Options \"" + name +
"\"\n " + optionState.getInceptionInformation());
}
}

View file

@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.
@ -17,30 +16,29 @@
package ghidra.framework.options;
/**
* Wrapper class for an object that represents a property value and is
* saved as a set of primitives.
* Wrapper class for an object that represents a property value and is saved as a set of primitives.
*/
public interface WrappedOption {
/**
* Get the object that is the property value.
* {@return Get the object that is the property value}
*/
public abstract Object getObject();
/**
* Concrete subclass of WrappedOption should read all of its
* state from the given saveState object.
* Subclasses of WrappedOption should read all state from the given save state object.
* @param saveState container of state information
*/
public abstract void readState(SaveState saveState);
/**
* Concrete subclass of WrappedOption should write all of its
* state to the given saveState object.
* Subclasses of WrappedOption should write all state to the given save state object.
* @param saveState container of state information
*/
public abstract void writeState(SaveState saveState);
/**
* {@return the option type}
*/
public abstract OptionType getOptionType();
}