diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/BasicPropertySupportFactoryTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/BasicPropertySupportFactoryTest.java new file mode 100644 index 00000000000..ae00c614902 --- /dev/null +++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/BasicPropertySupportFactoryTest.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.scout.rt.client.ui; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.eclipse.scout.rt.client.ui.action.AbstractAction; +import org.eclipse.scout.rt.client.ui.action.menu.AbstractMenu; +import org.eclipse.scout.rt.client.ui.action.menu.IMenu; +import org.eclipse.scout.rt.client.ui.basic.table.AbstractTable; +import org.eclipse.scout.rt.client.ui.basic.table.IHeaderCell; +import org.eclipse.scout.rt.client.ui.basic.table.ITable; +import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractColumn; +import org.eclipse.scout.rt.client.ui.form.fields.AbstractBasicField; +import org.eclipse.scout.rt.client.ui.form.fields.AbstractFormField; +import org.eclipse.scout.rt.client.ui.form.fields.AbstractValueField; +import org.eclipse.scout.rt.client.ui.form.fields.IBasicField; +import org.eclipse.scout.rt.client.ui.form.fields.IBasicFieldUIFacade; +import org.eclipse.scout.rt.client.ui.form.fields.IFormField; +import org.eclipse.scout.rt.client.ui.form.fields.IValueField; +import org.eclipse.scout.rt.client.ui.form.fields.button.AbstractButton; +import org.eclipse.scout.rt.client.ui.form.fields.button.IButton; +import org.eclipse.scout.rt.client.ui.form.fields.groupbox.AbstractGroupBox; +import org.eclipse.scout.rt.client.ui.form.fields.groupbox.IGroupBox; +import org.eclipse.scout.rt.platform.IgnoreBean; +import org.eclipse.scout.rt.platform.reflect.DefaultValueMap; +import org.eclipse.scout.rt.platform.util.ObjectUtility; +import org.eclipse.scout.rt.testing.platform.mock.RegisterBeanTestRule; +import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test that the {@link BasicPropertySupportFactory} more or less equal the actual defaults (e.g. changed defaults in the actual class should be reflected in this factory as well to avoid additional values pollution). + */ +@RunWith(PlatformTestRunner.class) +public class BasicPropertySupportFactoryTest { + + protected P_TestBasicPropertySupportFactory m_propertySupportFactory = new P_TestBasicPropertySupportFactory(); + + @Rule + public RegisterBeanTestRule m_propertySupportFactoryRule = new RegisterBeanTestRule(BasicPropertySupportFactory.class, () -> m_propertySupportFactory); + + @Test + public void testAbstractMenu() { + new AbstractMenu() { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + IMenu.PROP_MENU_TYPES // Set is always copied in setMenuTypes, improvement would be to just store the original Set (it is also copied on get operation) + ), additionalValues.keySet()); + } + + @Test + public void testAbstractColumn() { + new AbstractColumn() { + }; + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of(), additionalValues.keySet()); + } + + @Test + public void testAbstractButton() { + new AbstractButton() { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + IButton.PROP_GRID_DATA, + IButton.PROP_GRID_DATA_HINTS, + IButton.PROP_STATUS_MENU_MAPPINGS, // maybe use static empty list to create a new default (change in behavior)? + IButton.PROP_KEY_STROKES, // maybe use static empty list to create a new default (change in behavior)? + IButton.PROP_CONTEXT_MENU + ), additionalValues.keySet()); + } + + @Test + public void testAbstractGroupBox() { + new AbstractGroupBox() { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + IGroupBox.PROP_GRID_DATA, + IGroupBox.PROP_GRID_DATA_HINTS, + IGroupBox.PROP_STATUS_MENU_MAPPINGS, // maybe use static empty list to create a new default (change in behavior)? + IGroupBox.PROP_KEY_STROKES, // maybe use static empty list to create a new default (change in behavior)? + IGroupBox.PROP_CONTEXT_MENU, + IGroupBox.PROP_EMPTY, // usually group boxes are not empty, however ours here is + IGroupBox.PROP_HAS_VISIBLE_FIELDS, // usually group boxes should have fields, however ours does not + IGroupBox.PROP_GRID_COLUMN_COUNT, + IGroupBox.PROP_FIELDS, // maybe use static empty list to create a new default (change in behavior)? re-create on change of fields. + IGroupBox.PROP_BODY_LAYOUT_CONFIG, + IGroupBox.PROP_VISIBLE // usually group boxes are visible, ours is not + ), additionalValues.keySet()); + } + + @Test + public void testAbstractBasicField() { + new AbstractBasicField(true) { + @Override + public IBasicFieldUIFacade getUIFacade() { + return null; + } + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + IBasicField.PROP_GRID_DATA, + IBasicField.PROP_GRID_DATA_HINTS, + IBasicField.PROP_STATUS_MENU_MAPPINGS, // maybe use static empty list to create a new default (change in behavior)? + IBasicField.PROP_KEY_STROKES, // maybe use static empty list to create a new default (change in behavior)? + IBasicField.PROP_CONTEXT_MENU + ), additionalValues.keySet()); + } + + @Test + public void testAbstractValueField() { + new AbstractValueField(true) { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + IValueField.PROP_GRID_DATA, + IValueField.PROP_GRID_DATA_HINTS, + IValueField.PROP_STATUS_MENU_MAPPINGS, // maybe use static empty list to create a new default (change in behavior)? + IValueField.PROP_KEY_STROKES, // maybe use static empty list to create a new default (change in behavior)? + IValueField.PROP_CONTEXT_MENU + ), additionalValues.keySet()); + } + + @Test + public void testAbstractFormField() { + new AbstractFormField(true) { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + IFormField.PROP_GRID_DATA, + IFormField.PROP_GRID_DATA_HINTS, + IFormField.PROP_STATUS_MENU_MAPPINGS, // maybe use static empty list to create a new default (change in behavior)? + IFormField.PROP_KEY_STROKES, // maybe use static empty list to create a new default (change in behavior)? + IFormField.PROP_CONTEXT_MENU + ), additionalValues.keySet()); + } + + @Test + public void testAbstractAction() { + new AbstractAction() { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of(), additionalValues.keySet()); + } + + @Test + public void testAbstractTable() { + new AbstractTable() { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + ITable.PROP_KEY_STROKES, // maybe use static empty list to create a new default (change in behavior)? + ITable.PROP_CONTEXT_MENU, + ITable.PROP_TABLE_ORGANIZER, + ITable.PROP_USER_FILTER_MANAGER + ), additionalValues.keySet()); + } + + @Test + public void testAbstractWidget() { + new AbstractWidget() { + }.init(); + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getFirst(); + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of(), additionalValues.keySet()); + } + + @Test + public void testHeaderCell() { + new AbstractColumn() { + }; + P_DefaultValueMap lastDefaultValueMap = m_propertySupportFactory.getLastDefaultValueMaps().getLast(); // HeaderCell (created by column) + Map additionalValues = lastDefaultValueMap.getAdditionalValues(); + assertEquals(Set.of( + IHeaderCell.PROP_COLUMN_INDEX + ), additionalValues.keySet()); + } + + @Test + public void testCompleteness() { + Set testMethods = Arrays.stream(BasicPropertySupportFactoryTest.class.getDeclaredMethods()).map(Method::getName).collect(Collectors.toSet()); + Arrays.stream(BasicPropertySupportFactory.class.getDeclaredMethods()).filter(Predicate.not(Method::isSynthetic)).forEach(m -> { + String name = m.getName(); + if (name.startsWith("createDefaultValueMapFor") && testMethods.contains("test" + name.substring(24))) { + return; // method is tested + } + else if (ObjectUtility.isOneOf(name, "createDefaultValueMap", "createFor")) { + return; // tested implicitly by all test methods + } + fail("Shouldn't there be a test for " + m); + }); + } + + @IgnoreBean + protected class P_TestBasicPropertySupportFactory extends BasicPropertySupportFactory { + + private List m_lastDefaultValueMaps = new ArrayList<>(); + + @Override + protected Map createDefaultValueMap(Map defaultValues) { + P_DefaultValueMap defaultValueMap = new P_DefaultValueMap(defaultValues, true); + m_lastDefaultValueMaps.add(defaultValueMap); + return defaultValueMap; + } + + public List getLastDefaultValueMaps() { + return m_lastDefaultValueMaps; + } + } + + protected class P_DefaultValueMap extends DefaultValueMap { + + public P_DefaultValueMap(Map defaultValues, boolean startEmpty) { + super(defaultValues, startEmpty); + } + + protected Map getAdditionalValues() { + return m_additionalValues; + } + } +} diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/AbstractWidget.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/AbstractWidget.java index d78e52ba269..b16ed4013ad 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/AbstractWidget.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/AbstractWidget.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -21,11 +21,13 @@ import org.eclipse.scout.rt.client.ui.action.IAction; import org.eclipse.scout.rt.client.ui.desktop.IDesktop; +import org.eclipse.scout.rt.platform.BEANS; import org.eclipse.scout.rt.platform.Order; import org.eclipse.scout.rt.platform.annotations.ConfigProperty; import org.eclipse.scout.rt.platform.classid.ClassId; import org.eclipse.scout.rt.platform.holders.Holder; import org.eclipse.scout.rt.platform.reflect.AbstractPropertyObserver; +import org.eclipse.scout.rt.platform.reflect.BasicPropertySupport; import org.eclipse.scout.rt.platform.reflect.ConfigurationUtility; import org.eclipse.scout.rt.platform.util.CollectionUtility; import org.eclipse.scout.rt.platform.util.visitor.IBreadthFirstTreeVisitor; @@ -46,7 +48,7 @@ public abstract class AbstractWidget extends AbstractPropertyObserver implements private static final Logger LOG = LoggerFactory.getLogger(AbstractWidget.class); private static final NamedBitMaskHelper ENABLED_BIT_HELPER = new NamedBitMaskHelper(IDimensions.ENABLED, IDimensions.ENABLED_GRANTED, IDimensions.ENABLED_SLAVE); - private static final String PROP_ENABLED_BYTE = "enabledByte"; + public static final String PROP_ENABLED_BYTE = "enabledByte"; private final WidgetListeners m_listenerList; @@ -70,6 +72,11 @@ protected final void callInitializer() { setInitConfigDone(true); } + @Override + protected BasicPropertySupport createPropertySupport() { + return BEANS.get(BasicPropertySupportFactory.class).createFor(this); + } + /** * Will be called by {@link #callInitializer()} but only if {@link #isInitConfigDone()} returns false. */ diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/BasicPropertySupportFactory.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/BasicPropertySupportFactory.java new file mode 100644 index 00000000000..71e87eb69cd --- /dev/null +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/BasicPropertySupportFactory.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.scout.rt.client.ui; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.scout.rt.client.ui.action.AbstractAction; +import org.eclipse.scout.rt.client.ui.action.IAction; +import org.eclipse.scout.rt.client.ui.action.menu.AbstractMenu; +import org.eclipse.scout.rt.client.ui.action.menu.IMenu; +import org.eclipse.scout.rt.client.ui.basic.table.AbstractTable; +import org.eclipse.scout.rt.client.ui.basic.table.CheckableStyle; +import org.eclipse.scout.rt.client.ui.basic.table.GroupingStyle; +import org.eclipse.scout.rt.client.ui.basic.table.HeaderCell; +import org.eclipse.scout.rt.client.ui.basic.table.HierarchicalStyle; +import org.eclipse.scout.rt.client.ui.basic.table.ITable; +import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractColumn; +import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn; +import org.eclipse.scout.rt.client.ui.form.fields.AbstractBasicField; +import org.eclipse.scout.rt.client.ui.form.fields.AbstractFormField; +import org.eclipse.scout.rt.client.ui.form.fields.AbstractValueField; +import org.eclipse.scout.rt.client.ui.form.fields.IBasicField; +import org.eclipse.scout.rt.client.ui.form.fields.IFormField; +import org.eclipse.scout.rt.client.ui.form.fields.IValueField; +import org.eclipse.scout.rt.client.ui.form.fields.button.AbstractButton; +import org.eclipse.scout.rt.client.ui.form.fields.button.IButton; +import org.eclipse.scout.rt.client.ui.form.fields.groupbox.AbstractGroupBox; +import org.eclipse.scout.rt.client.ui.form.fields.groupbox.IGroupBox; +import org.eclipse.scout.rt.client.ui.form.fields.stringfield.IStringField; +import org.eclipse.scout.rt.platform.ApplicationScoped; +import org.eclipse.scout.rt.platform.IOrdered; +import org.eclipse.scout.rt.platform.config.CONFIG; +import org.eclipse.scout.rt.platform.reflect.AbstractPropertyObserver; +import org.eclipse.scout.rt.platform.reflect.AbstractPropertyObserver.StoreConfigValuesConfigProperty; +import org.eclipse.scout.rt.platform.reflect.BasicPropertySupport; +import org.eclipse.scout.rt.platform.reflect.DefaultValueMap; +import org.eclipse.scout.rt.platform.util.CollectionUtility; +import org.eclipse.scout.rt.platform.util.ImmutablePair; +import org.eclipse.scout.rt.platform.util.TriState; + +/** + * Factory to create a {@link BasicPropertySupport} for any {@link AbstractPropertyObserver}. + *

+ * The newly created property support will always be backed by an underlying empty {@link Map}, this is either a regular {@link HashMap} or a {@link DefaultValueMap} which is size-optimized for the specific property observer if it is filled + * with the default values for this observer. In any case this does not result in different behavior, the only difference will be in performance and memory consumption. + *

+ */ +@ApplicationScoped +public class BasicPropertySupportFactory { + + public Map, Map> m_defaultValuesByClass = new HashMap<>(); + + public BasicPropertySupport createFor(AbstractPropertyObserver holder) { + // order matters + Map defaultValues = switch (holder) { + case AbstractMenu ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractMenu.class, k -> createDefaultValueMapForAbstractMenu()); + case AbstractColumn ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractColumn.class, k -> createDefaultValueMapForAbstractColumn()); + case AbstractButton ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractButton.class, k -> createDefaultValueMapForAbstractButton()); + case AbstractGroupBox ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractGroupBox.class, k -> createDefaultValueMapForAbstractGroupBox()); + case AbstractBasicField ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractBasicField.class, k -> createDefaultValueMapForAbstractBasicField()); + case AbstractValueField ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractValueField.class, k -> createDefaultValueMapForAbstractValueField()); + case AbstractFormField ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractFormField.class, k -> createDefaultValueMapForAbstractFormField()); + case AbstractAction ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractAction.class, k -> createDefaultValueMapForAbstractAction()); + case AbstractTable ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractTable.class, k -> createDefaultValueMapForAbstractTable()); + case AbstractWidget ignored -> m_defaultValuesByClass.computeIfAbsent(AbstractWidget.class, k -> createDefaultValueMapForAbstractWidget()); // fallback: AbstractWidget + case HeaderCell ignored -> m_defaultValuesByClass.computeIfAbsent(HeaderCell.class, k -> createDefaultValueMapForHeaderCell()); + default -> null; + }; + return new BasicPropertySupport(holder, defaultValues != null ? createDefaultValueMap(defaultValues) : new HashMap<>()); + } + + protected Map createDefaultValueMap(Map defaultValues) { + return new DefaultValueMap(defaultValues, true); + } + + protected Map createDefaultValueMapForHeaderCell() { + return Collections.unmodifiableMap(CollectionUtility.hashMap( + new ImmutablePair<>(HeaderCell.PROP_TEXT, null), + new ImmutablePair<>(HeaderCell.PROP_TOOLTIP_HTML_ENABLED, false), + new ImmutablePair<>(HeaderCell.PROP_ICON_ID, null), + new ImmutablePair<>(HeaderCell.PROP_CSS_CLASS, null), + new ImmutablePair<>(HeaderCell.PROP_HTML_ENABLED, false), + new ImmutablePair<>(HeaderCell.PROP_MENU_ENABLED, true), + new ImmutablePair<>(HeaderCell.PROP_HORIZONTAL_ALIGNMENT, -1) + )); + } + + protected Map createDefaultValueMapForAbstractColumn() { + return Collections.unmodifiableMap(CollectionUtility.hashMap( + new ImmutablePair<>(IColumn.PROP_AUTO_OPTIMIZE_WIDTH, false), + new ImmutablePair<>(IColumn.PROP_HORIZONTAL_ALIGNMENT, -1), + new ImmutablePair<>(IColumn.PROP_VISIBLE, true), + new ImmutablePair<>(IColumn.PROP_ORDER, IOrdered.DEFAULT_ORDER), + new ImmutablePair<>(IColumn.PROP_WIDTH, 60), + new ImmutablePair<>(IColumn.PROP_MIN_WIDTH, 60), + new ImmutablePair<>(IColumn.PROP_AUTO_OPTIMIZE_MAX_WIDTH, -1), + new ImmutablePair<>(IColumn.PROP_FIXED_WIDTH, false), + new ImmutablePair<>(IColumn.PROP_FIXED_POSITION, false), + new ImmutablePair<>(IColumn.PROP_EDITABLE, false), + new ImmutablePair<>(IFormField.PROP_MANDATORY, false), + new ImmutablePair<>(IColumn.PROP_VIEW_COLUMN_INDEX_HINT, -1), + new ImmutablePair<>(IColumn.PROP_CSS_CLASS, null), + new ImmutablePair<>(IStringField.PROP_WRAP_TEXT, false), + new ImmutablePair<>(IColumn.PROP_HTML_ENABLED, false), + new ImmutablePair<>(IColumn.PROP_UI_SORT_POSSIBLE, false), + new ImmutablePair<>(IColumn.PROP_NODE_COLUMN_CANDIDATE, true) + )); + } + + protected Map createDefaultValueMapForAbstractButton() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractFormField()); + defaultValues.put(IButton.PROP_INHERIT_ACCESSIBILITY, true); + defaultValues.put(IButton.PROP_DISABLED_STYLE, IButton.DISPLAY_STYLE_DEFAULT); + defaultValues.put(IButton.PROP_DEFAULT_BUTTON, null); + defaultValues.put(IButton.PROP_ICON_ID, null); + defaultValues.put(IButton.PROP_KEY_STROKE, null); + defaultValues.put(IButton.PROP_KEY_STROKE_SCOPE_CLASS, null); + defaultValues.put(IButton.PROP_PREVENT_DOUBLE_CLICK, false); + defaultValues.put(IButton.PROP_STACKABLE, true); + defaultValues.put(IButton.PROP_SHRINKABLE, false); + defaultValues.put(IFormField.PROP_STATUS_VISIBLE, false); // default is overwritten for button + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractGroupBox() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractFormField()); + defaultValues.put(IGroupBox.PROP_SUB_LABEL, null); + defaultValues.put(IGroupBox.PROP_EXPANDABLE, false); + defaultValues.put(IGroupBox.PROP_EXPANDED, true); + defaultValues.put(IGroupBox.PROP_CACHE_EXPANDED, false); + defaultValues.put(IGroupBox.PROP_BORDER_VISIBLE, true); + defaultValues.put(IGroupBox.PROP_BORDER_DECORATION, IGroupBox.BORDER_DECORATION_AUTO); + defaultValues.put(IGroupBox.PROP_GRID_COLUMN_COUNT, 2); // default value -1 => 2 + defaultValues.put(IGroupBox.PROP_BACKGROUND_IMAGE_NAME, null); + defaultValues.put(IGroupBox.PROP_BACKGROUND_IMAGE_HORIZONTAL_ALIGNMENT, 0); + defaultValues.put(IGroupBox.PROP_BACKGROUND_IMAGE_VERTICAL_ALIGNMENT, 0); + defaultValues.put(IGroupBox.PROP_EMPTY, false); // usually group boxes are not empty (even though they are initially empty) + defaultValues.put(IGroupBox.PROP_SCROLLABLE, TriState.UNDEFINED); + defaultValues.put(IGroupBox.PROP_SELECTION_KEYSTROKE, null); + defaultValues.put(IGroupBox.PROP_MENU_BAR_POSITION, IGroupBox.MENU_BAR_POSITION_AUTO); + defaultValues.put(IGroupBox.PROP_MENU_BAR_ELLIPSIS_POSITION, IGroupBox.MENU_BAR_ELLIPSIS_POSITION_RIGHT); + defaultValues.put(IGroupBox.PROP_RESPONSIVE, TriState.UNDEFINED); + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractBasicField() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractValueField()); + defaultValues.put(IBasicField.PROP_UPDATE_DISPLAY_TEXT_ON_MODIFY, false); + defaultValues.put(IBasicField.PROP_UPDATE_DISPLAY_TEXT_ON_MODIFY_DELAY, 250); + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractValueField() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractFormField()); + defaultValues.put(IValueField.PROP_CLEARABLE, IValueField.CLEARABLE_FOCUSED); + defaultValues.put(IValueField.PROP_AUTO_ADD_DEFAULT_MENUS, true); + defaultValues.put(IValueField.PROP_DISPLAY_TEXT, ""); + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractFormField() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractWidget()); + defaultValues.put(IFormField.PROP_EMPTY, true); + defaultValues.put(IFormField.PROP_FIELD_STYLE, IFormField.FIELD_STYLE_ALTERNATIVE); + defaultValues.put(IFormField.PROP_DISABLED_STYLE, IFormField.DISABLED_STYLE_DEFAULT); + defaultValues.put(IFormField.PROP_VISIBLE, true); + defaultValues.put(IFormField.PROP_MANDATORY, false); + defaultValues.put(IFormField.PROP_ORDER, IOrdered.DEFAULT_ORDER); + defaultValues.put(IFormField.PROP_TOOLTIP_TEXT, null); + defaultValues.put(IFormField.PROP_TOOLTIP_ANCHOR, IFormField.TOOLTIP_ANCHOR_DEFAULT); + defaultValues.put(IFormField.PROP_LABEL, null); + defaultValues.put(IFormField.PROP_LABEL_POSITION, IFormField.LABEL_POSITION_DEFAULT); + defaultValues.put(IFormField.PROP_LABEL_WIDTH_IN_PIXEL, IFormField.LABEL_WIDTH_DEFAULT); + defaultValues.put(IFormField.PROP_LABEL_USE_UI_WIDTH, false); + defaultValues.put(IFormField.PROP_LABEL_VISIBLE, true); + defaultValues.put(IFormField.PROP_LABEL_HTML_ENABLED, false); + defaultValues.put(IFormField.PROP_STATUS_VISIBLE, true); + defaultValues.put(IFormField.PROP_STATUS_POSITION, IFormField.STATUS_POSITION_DEFAULT); + defaultValues.put(IFormField.PROP_CSS_CLASS, null); + defaultValues.put(IFormField.PROP_PREVENT_INITIAL_FOCUS, false); + defaultValues.put(IFormField.PROP_SAVE_NEEDED, false); + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractMenu() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractAction()); + if (Boolean.TRUE.equals(CONFIG.getPropertyValue(StoreConfigValuesConfigProperty.class))) { + defaultValues.put(IMenu.PROP_MENU_TYPES, AbstractMenu.DEFAULT_MENU_TYPES); + defaultValues.put(IMenu.PROP_PREVENT_DOUBLE_CLICK, false); + defaultValues.put(IMenu.PROP_STACKABLE, true); + defaultValues.put(IMenu.PROP_SHRINKABLE, false); + defaultValues.put(IMenu.PROP_SUB_MENU_VISIBILITY, IMenu.SUB_MENU_VISIBILITY_DEFAULT); + } + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractAction() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractWidget()); + defaultValues.put(IAction.PROP_ICON_ID, null); + defaultValues.put(IAction.PROP_TEXT, null); + defaultValues.put(IAction.PROP_TEXT_POSITION, IAction.TEXT_POSITION_DEFAULT); + defaultValues.put(IAction.PROP_HTML_ENABLED, false); + defaultValues.put(IAction.PROP_TOOLTIP_TEXT, null); + defaultValues.put(IAction.PROP_KEY_STROKE, null); + defaultValues.put(IAction.PROP_KEYSTROKE_FIRE_POLICY, IAction.KEYSTROKE_FIRE_POLICY_ACCESSIBLE_ONLY); + defaultValues.put(IAction.PROP_VISIBLE, true); + defaultValues.put(IAction.PROP_ORDER, IOrdered.DEFAULT_ORDER); + defaultValues.put(IAction.PROP_ACTION_STYLE, IMenu.ACTION_STYLE_DEFAULT); + defaultValues.put(IAction.PROP_CSS_CLASS, null); + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractTable() { + HashMap defaultValues = new HashMap<>(createDefaultValueMapForAbstractWidget()); + defaultValues.put(ITable.PROP_LOADING, false); + defaultValues.put(ITable.PROP_GROUPING_STYLE, GroupingStyle.TOP); + defaultValues.put(ITable.PROP_HIERARCHICAL_STYLE, HierarchicalStyle.DEFAULT); + defaultValues.put(ITable.PROP_CHECKABLE_STYLE, CheckableStyle.CHECKBOX); + defaultValues.put(ITable.PROP_TITLE, null); + defaultValues.put(ITable.PROP_SORT_ENABLED, true); + defaultValues.put(ITable.PROP_DEFAULT_ICON, null); + defaultValues.put(ITable.PROP_CSS_CLASS, null); + defaultValues.put(ITable.PROP_ROW_ICON_VISIBLE, false); + defaultValues.put(ITable.PROP_ROW_ICON_COLUMN_WIDTH, IColumn.NARROW_MIN_WIDTH); + defaultValues.put(ITable.PROP_HEADER_VISIBLE, true); + defaultValues.put(ITable.PROP_HEADER_ENABLED, true); + defaultValues.put(ITable.PROP_HEADER_MENUS_ENABLED, true); + defaultValues.put(ITable.PROP_AUTO_RESIZE_COLUMNS, false); + defaultValues.put(ITable.PROP_CHECKABLE, false); + defaultValues.put(ITable.PROP_MULTI_CHECK, true); + defaultValues.put(ITable.PROP_MULTI_SELECT, true); + defaultValues.put(ITable.PROP_MULTILINE_TEXT, false); + defaultValues.put(ITable.PROP_KEYBOARD_NAVIGATION, true); + defaultValues.put(ITable.PROP_DRAG_TYPE, 0); + defaultValues.put(ITable.PROP_DROP_TYPE, 0); + defaultValues.put(ITable.PROP_DROP_MAXIMUM_SIZE, ITable.DEFAULT_DROP_MAXIMUM_SIZE); + defaultValues.put(ITable.PROP_SCROLL_TO_SELECTION, false); + defaultValues.put(ITable.PROP_TABLE_STATUS_VISIBLE, false); + defaultValues.put(ITable.PROP_TEXT_FILTER_ENABLED, true); + defaultValues.put(ITable.PROP_TRUNCATED_CELL_TOOLTIP_ENABLED, TriState.UNDEFINED); + defaultValues.put(ITable.PROP_CLIENT_UI_PREFERENCES_ENABLED, true); + defaultValues.put(ITable.PROP_TILE_MODE, false); + defaultValues.put(ITable.PROP_COMPACT, false); + defaultValues.put(ITable.PROP_TABLE_CUSTOMIZER, null); + return Collections.unmodifiableMap(defaultValues); + } + + protected Map createDefaultValueMapForAbstractWidget() { + return Collections.unmodifiableMap(CollectionUtility.hashMap( + new ImmutablePair<>(IWidget.PROP_INIT_CONFIG_DONE, true), + new ImmutablePair<>(IWidget.PROP_INIT_DONE, true), + new ImmutablePair<>(AbstractWidget.PROP_ENABLED_BYTE, (byte) -1), + new ImmutablePair<>(IWidget.PROP_INHERIT_ACCESSIBILITY, true), + new ImmutablePair<>(IWidget.PROP_CSS_CLASS, null), + new ImmutablePair<>(IWidget.PROP_DISPOSE_DONE, false) + )); + } +} diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/action/menu/AbstractMenu.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/action/menu/AbstractMenu.java index 44484af390a..f15d816d3f9 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/action/menu/AbstractMenu.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/action/menu/AbstractMenu.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -36,7 +36,7 @@ @ClassId("e8dbfee4-503c-401e-8579-d0aa8618f59d") public abstract class AbstractMenu extends AbstractActionNode implements IMenu { - private static final Set DEFAULT_MENU_TYPES = Set.of( + public static final Set DEFAULT_MENU_TYPES = Set.of( TableMenuType.SingleSelection, TreeMenuType.SingleSelection, ValueFieldMenuType.NotNull, diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/HeaderCell.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/HeaderCell.java index 924b9f75f3c..b4a95547586 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/HeaderCell.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/HeaderCell.java @@ -9,7 +9,10 @@ */ package org.eclipse.scout.rt.client.ui.basic.table; +import org.eclipse.scout.rt.client.ui.BasicPropertySupportFactory; +import org.eclipse.scout.rt.platform.BEANS; import org.eclipse.scout.rt.platform.reflect.AbstractPropertyObserver; +import org.eclipse.scout.rt.platform.reflect.BasicPropertySupport; import org.eclipse.scout.rt.shared.data.basic.FontSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +25,11 @@ public HeaderCell() { setHorizontalAlignment(-1); } + @Override + protected BasicPropertySupport createPropertySupport() { + return BEANS.get(BasicPropertySupportFactory.class).createFor(this); + } + @Override public int getColumnIndex() { return propertySupport.getPropertyInt(PROP_COLUMN_INDEX); diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractColumn.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractColumn.java index c048ec47858..66af9ff05e3 100644 --- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractColumn.java +++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/columns/AbstractColumn.java @@ -28,6 +28,7 @@ import org.eclipse.scout.rt.client.extension.ui.basic.table.columns.ColumnChains.ColumnValidateValueChain; import org.eclipse.scout.rt.client.extension.ui.basic.table.columns.IColumnExtension; import org.eclipse.scout.rt.client.services.common.icon.IIconProviderService; +import org.eclipse.scout.rt.client.ui.BasicPropertySupportFactory; import org.eclipse.scout.rt.client.ui.ClientUIPreferences; import org.eclipse.scout.rt.client.ui.basic.cell.Cell; import org.eclipse.scout.rt.client.ui.basic.cell.ICell; @@ -49,6 +50,7 @@ import org.eclipse.scout.rt.client.ui.form.fields.ValidationFailedStatus; import org.eclipse.scout.rt.client.ui.form.fields.stringfield.IStringField; import org.eclipse.scout.rt.client.ui.form.fields.tablefield.AbstractTableField; +import org.eclipse.scout.rt.platform.BEANS; import org.eclipse.scout.rt.platform.IOrdered; import org.eclipse.scout.rt.platform.Order; import org.eclipse.scout.rt.platform.Replace; @@ -58,6 +60,7 @@ import org.eclipse.scout.rt.platform.exception.ProcessingException; import org.eclipse.scout.rt.platform.holders.IHolder; import org.eclipse.scout.rt.platform.reflect.AbstractPropertyObserver; +import org.eclipse.scout.rt.platform.reflect.BasicPropertySupport; import org.eclipse.scout.rt.platform.reflect.ConfigurationUtility; import org.eclipse.scout.rt.platform.status.IMultiStatus; import org.eclipse.scout.rt.platform.util.Assertions; @@ -153,6 +156,11 @@ protected boolean isInitialized() { return FLAGS_BIT_HELPER.isBitSet(INITIALIZED, m_flags); } + @Override + protected BasicPropertySupport createPropertySupport() { + return BEANS.get(BasicPropertySupportFactory.class).createFor(this); + } + protected Map getPropertiesMap() { return propertySupport.getPropertiesMap(); } diff --git a/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/reflect/DefaultValueMapTest.java b/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/reflect/DefaultValueMapTest.java new file mode 100644 index 00000000000..d412e6c51cc --- /dev/null +++ b/org.eclipse.scout.rt.platform.test/src/test/java/org/eclipse/scout/rt/platform/reflect/DefaultValueMapTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.scout.rt.platform.reflect; + +import static org.junit.Assert.*; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; + +public class DefaultValueMapTest { + + @Test + public void testIsEmptyAndSize() { + DefaultValueMap mapWithA = new DefaultValueMap(Map.of("A", 1)); + assertEquals(1, mapWithA.size()); + assertFalse(mapWithA.isEmpty()); + + mapWithA.put("B", 2); + assertEquals(2, mapWithA.size()); + assertFalse(mapWithA.isEmpty()); + + mapWithA.put("C", 3); + assertEquals(3, mapWithA.size()); + assertFalse(mapWithA.isEmpty()); + + assertNotNull(mapWithA.put("A", 4)); + assertEquals(3, mapWithA.size()); + assertFalse(mapWithA.isEmpty()); + + mapWithA.remove("B"); + assertEquals(2, mapWithA.size()); + assertFalse(mapWithA.isEmpty()); + + mapWithA.remove("A"); + assertEquals(1, mapWithA.size()); + assertFalse(mapWithA.isEmpty()); + + mapWithA.remove("C"); + assertEquals(0, mapWithA.size()); + assertTrue(mapWithA.isEmpty()); + + mapWithA = new DefaultValueMap(Map.of("A", 1)); + mapWithA.remove("A"); + assertEquals(0, mapWithA.size()); + assertTrue(mapWithA.isEmpty()); + + DefaultValueMap emptyMap = new DefaultValueMap(Collections.emptyMap()); + assertEquals(0, emptyMap.size()); + assertTrue(emptyMap.isEmpty()); + + emptyMap.put("A", 1); + assertEquals(1, emptyMap.size()); + assertFalse(emptyMap.isEmpty()); + } + + @Test + public void testKeySet() { + DefaultValueMap mapWithA = new DefaultValueMap(Map.of("A", 1)); + assertEquals(Set.of("A"), mapWithA.keySet()); + + mapWithA.put("B", 2); + assertEquals(Set.of("A", "B"), mapWithA.keySet()); + + mapWithA.put("C", 3); + assertEquals(Set.of("A", "B", "C"), mapWithA.keySet()); + + assertNotNull(mapWithA.put("A", 4)); + assertEquals(Set.of("A", "B", "C"), mapWithA.keySet()); + + mapWithA.remove("B"); + assertEquals(Set.of("A", "C"), mapWithA.keySet()); + + mapWithA.remove("A"); + assertEquals(Set.of("C"), mapWithA.keySet()); + + mapWithA.remove("C"); + assertEquals(Set.of(), mapWithA.keySet()); + + mapWithA = new DefaultValueMap(Map.of("A", 1)); + mapWithA.remove("A"); + assertEquals(Set.of(), mapWithA.keySet()); + + DefaultValueMap emptyMap = new DefaultValueMap(Collections.emptyMap()); + assertEquals(Set.of(), emptyMap.keySet()); + + emptyMap.put("A", 1); + assertEquals(Set.of("A"), emptyMap.keySet()); + } + + @Test + public void testValues() { + DefaultValueMap mapWithA = new DefaultValueMap(Map.of("A", 1)); + assertEquals(Set.of(1), new HashSet<>(mapWithA.values())); + + mapWithA.put("B", 2); + assertEquals(Set.of(1, 2), new HashSet<>(mapWithA.values())); + + mapWithA.put("C", 3); + assertEquals(Set.of(1, 2, 3), new HashSet<>(mapWithA.values())); + + assertNotNull(mapWithA.put("A", 4)); + assertEquals(Set.of(2, 3, 4), new HashSet<>(mapWithA.values())); + + mapWithA.remove("B"); + assertEquals(Set.of(3, 4), new HashSet<>(mapWithA.values())); + + mapWithA.remove("A"); + assertEquals(Set.of(3), new HashSet<>(mapWithA.values())); + + mapWithA.remove("C"); + assertEquals(Set.of(), new HashSet<>(mapWithA.values())); + + mapWithA = new DefaultValueMap(Map.of("A", 1)); + mapWithA.remove("A"); + assertEquals(Set.of(), new HashSet<>(mapWithA.values())); + + DefaultValueMap emptyMap = new DefaultValueMap(Collections.emptyMap()); + assertEquals(Set.of(), new HashSet<>(emptyMap.values())); + + emptyMap.put("A", 1); + assertEquals(Set.of(1), new HashSet<>(emptyMap.values())); + } + + @Test + public void testRemoveByKeySet() { + DefaultValueMap mapWithA = new DefaultValueMap(Map.of("A", 1)); + mapWithA.put("B", 2); + mapWithA.put("C", 3); + + mapWithA.keySet().remove("A"); + assertEquals(Set.of("B", "C"), mapWithA.keySet()); + + mapWithA.keySet().remove("C"); + assertEquals(Set.of("B"), mapWithA.keySet()); + + mapWithA.put("A", 4); + assertEquals(Set.of("A", "B"), mapWithA.keySet()); + + mapWithA.keySet().remove("A"); + assertEquals(Set.of("B"), mapWithA.keySet()); + } + + @Test + public void testGetAndPut() { + DefaultValueMap mapWithA = new DefaultValueMap(Map.of("A", 1)); + assertEquals(1, mapWithA.get("A")); + assertNull(mapWithA.get("B")); + + mapWithA.put("B", 2); + assertEquals(2, mapWithA.get("B")); + + mapWithA.remove("A"); + assertNull(mapWithA.get("A")); + + mapWithA.put("A", 1); + assertEquals(1, mapWithA.get("A")); + + mapWithA.put("A", 3); + assertEquals(3, mapWithA.get("A")); + } + + @Test + public void testPutAll() { + DefaultValueMap mapWithA = new DefaultValueMap(Map.of("A", 1)); + assertEquals(1, mapWithA.size()); + + mapWithA.putAll(Map.of("B", 2, "C", 3)); + assertEquals(3, mapWithA.size()); + } +} diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/AbstractPropertyObserver.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/AbstractPropertyObserver.java index 33fb8051a45..1813f2c5e86 100644 --- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/AbstractPropertyObserver.java +++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/AbstractPropertyObserver.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -23,7 +23,15 @@ public abstract class AbstractPropertyObserver implements IPropertyObserver { private static final LazyValue STORE_CONFIG_VALUES = new LazyValue<>(() -> CONFIG.getPropertyValue(StoreConfigValuesConfigProperty.class)); @SuppressWarnings("squid:S00116") - protected final BasicPropertySupport propertySupport = new BasicPropertySupport(this); + protected final BasicPropertySupport propertySupport; + + public AbstractPropertyObserver() { + propertySupport = createPropertySupport(); + } + + protected BasicPropertySupport createPropertySupport() { + return new BasicPropertySupport(this); + } @Override public void addPropertyChangeListener(PropertyChangeListener listener) { diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/BasicPropertySupport.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/BasicPropertySupport.java index 6bcf1486592..25ab982e276 100644 --- a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/BasicPropertySupport.java +++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/BasicPropertySupport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023 BSI Business Systems Integration AG + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -47,7 +47,7 @@ public class BasicPropertySupport implements IListenerListWithManagement { public static final long DEFAULT_LONG_VALUE = DEFAULT_INT_VALUE; public static final Long DEFAULT_LONG = DEFAULT_LONG_VALUE; private static final Boolean DEFAULT_BOOL = Boolean.FALSE; - private final Map m_props = new HashMap<>(); + private final Map m_props; private final Object m_source; // observer private final Object m_listenerLock = new Object(); @@ -57,7 +57,17 @@ public class BasicPropertySupport implements IListenerListWithManagement { private List m_propertyEventBuffer; public BasicPropertySupport(Object sourceBean) { + this(sourceBean, new HashMap<>()); + } + + /** + * + * @param propertyMap + * either pre-filled or empty + */ + public BasicPropertySupport(Object sourceBean, Map propertyMap) { m_source = sourceBean; + m_props = propertyMap; ListenerListRegistry.globalInstance().registerAsWeakReference(this); } @@ -564,7 +574,7 @@ private void processChangeBuffer() { // reverse traversal for (int i = a.length - 1; i >= 0; i--) { if (!names.contains(a[i].getPropertyName())) { - coalesceList.add(0, a[i]); + coalesceList.addFirst(a[i]); names.add(a[i].getPropertyName()); } } diff --git a/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/DefaultValueMap.java b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/DefaultValueMap.java new file mode 100644 index 00000000000..2fb3e515bf7 --- /dev/null +++ b/org.eclipse.scout.rt.platform/src/main/java/org/eclipse/scout/rt/platform/reflect/DefaultValueMap.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2010, 2026 BSI Business Systems Integration AG + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.scout.rt.platform.reflect; + +import java.lang.constant.Constable; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * {@link Map} backed by another {@link Map} with default values (this {@link Map} will remain unchanged but allows using a static {@link Map} for multiple instances to reduce memory consumption). + */ +public class DefaultValueMap extends AbstractMap { + + /** + * Static object put into {@link #m_additionalValues} to mark an object contained in the {@link #m_defaultValues} as removed. + */ + protected static final Object REMOVED_MARKER = new Object(); + + /** + * Default values contained in many instances of this class. + */ + protected final Map m_defaultValues; + + /** + * Additional or changed values, values in this {@link Map} overwrite values in the {@link #m_defaultValues}. + */ + protected final Map m_additionalValues; + + /** + * + * @param defaultValues + * pre-filled entries for this {@link Map} + */ + public DefaultValueMap(Map defaultValues) { + this(defaultValues, false); + } + + /** + * + * @param defaultValues + * potential default values for this {@link Map} + * @param startEmpty + * whether {@link Map} is initially empty (true) or should be pre-filled with the default values + */ + public DefaultValueMap(Map defaultValues, boolean startEmpty) { + m_defaultValues = defaultValues; + m_additionalValues = new HashMap<>(); + + if (startEmpty) { + // mark all default values as removed in the initial values + // assume the default values are filled shortly after creation otherwise this instance is going to consume more memory than a regular HashMap + m_defaultValues.keySet().forEach(k -> m_additionalValues.put(k, REMOVED_MARKER)); + } + } + + @Override + public int size() { + return m_defaultValues.keySet().stream() + .mapToInt(k -> m_additionalValues.containsKey(k) ? 0 : 1) + .sum() // all default values as long they are not contained in the additional values + + m_additionalValues.values().stream() + .mapToInt(o -> o == REMOVED_MARKER ? 0 : 1) + .sum(); // all additional values as long as they are not removed + } + + @Override + public boolean isEmpty() { + return (m_defaultValues.isEmpty() && m_additionalValues.isEmpty()) // either default and additional values are empty + || size() == 0; // or size is 0 + } + + @Override + public boolean containsKey(Object key) { + boolean additionalValuesContainsKey = m_additionalValues.containsKey(key); + //noinspection SuspiciousMethodCalls + return (m_defaultValues.containsKey(key) && !additionalValuesContainsKey) || (additionalValuesContainsKey && m_additionalValues.get(key) != REMOVED_MARKER); + } + + @Override + public boolean containsValue(Object value) { + return m_defaultValues.entrySet().stream().anyMatch(e -> Objects.equals(e.getValue(), value) && !m_additionalValues.containsKey(e.getKey())) || m_additionalValues.containsValue(value); + } + + @Override + public Object get(Object key) { + //noinspection SuspiciousMethodCalls + if (m_additionalValues.containsKey(key)) { + Object value = m_additionalValues.get(key); + return value == REMOVED_MARKER ? null : value; + } + // either contained here, might return null if not contained + return m_defaultValues.get(key); + } + + @Override + public Object put(String key, Object value) { + boolean additionalValuesContainsKey = m_additionalValues.containsKey(key); + if (m_defaultValues.containsKey(key)) { + if (isDefaultValue(key, value)) { // must be same, equals would not be sufficient + return additionalValuesContainsKey + ? Optional.ofNullable(m_additionalValues.remove(key)).filter(p -> p != REMOVED_MARKER).orElse(null) + : m_defaultValues.get(key); + } + else if (!additionalValuesContainsKey) { + m_additionalValues.put(key, value); + return m_defaultValues.get(key); // load previous value from default values (if any) + } + } + return Optional.ofNullable(m_additionalValues.put(key, value)).filter(p -> p != REMOVED_MARKER).orElse(null); + } + + protected boolean isDefaultValue(String key, Object value) { + Object defaultValue = m_defaultValues.get(key); + if (defaultValue == value) { + return true; + } + else if ((defaultValue == null && value != null) || (defaultValue != null && value == null)) { + return false; // null-safety for following code + } + if (defaultValue instanceof Constable) { + return Objects.equals(defaultValue, value); // equals is alright for constant types + } + return false; + } + + @Override + public Set> entrySet() { + return new AbstractSet<>() { + + @Override + public Iterator> iterator() { + Iterator> iterator = new Iterator<>() { + private Entry current = null; + private String lastKey = null; + private boolean advanced = false; + + private Iterator> defaultValuesEntries = m_defaultValues.entrySet().iterator(); + private Iterator> additionalValuesEntries; + + @Override + public boolean hasNext() { + if (!advanced) { + advanceInternal(); + } + return current != null; + } + + private void advanceInternal() { + Object previous = current; + // advance through default values + while (defaultValuesEntries != null && defaultValuesEntries.hasNext() && (previous == current || m_additionalValues.containsKey(current.getKey()))) { // skip the default values contained as additional values + current = defaultValuesEntries.next(); + } + if ((previous == current || m_additionalValues.containsKey(current.getKey())) && defaultValuesEntries != null) { + defaultValuesEntries = null; // default values entries are all consumed, not necessary anymore + additionalValuesEntries = m_additionalValues.entrySet().iterator(); // now consume the additional values entries + previous = current; + } + // advance through additional values + while (additionalValuesEntries != null && additionalValuesEntries.hasNext() && (previous == current || m_additionalValues.get(current.getKey()) == REMOVED_MARKER)) { // skip the additional values marked as removed + current = additionalValuesEntries.next(); + } + if (previous == current || m_additionalValues.get(current.getKey()) == REMOVED_MARKER) { + current = null; + } + advanced = true; + } + + @Override + public Entry next() { + if (!advanced) { + advanceInternal(); + } + advanced = false; + if (current == null) { + throw new NoSuchElementException(); + } + lastKey = current.getKey(); + // return the previous entry + return current; + } + + @Override + public void remove() { + if (lastKey == null) { + throw new IllegalStateException(); + } + if (m_defaultValues.containsKey(lastKey)) { + // potentially overwrite previously set value (previous value might not have been the default value) + m_additionalValues.put(lastKey, REMOVED_MARKER); + } + else { + additionalValuesEntries.remove(); + } + lastKey = null; + } + }; + return iterator; + } + + @Override + public int size() { + return DefaultValueMap.this.size(); + } + + @Override + public boolean isEmpty() { + return DefaultValueMap.this.isEmpty(); + } + }; + } +}