/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2015 ForgeRock AS */ package org.forgerock.opendj.config.dsconfig; import static com.forgerock.opendj.cli.ReturnCode.*; import static com.forgerock.opendj.dsconfig.DsconfigMessages.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizableMessageBuilder; import org.forgerock.opendj.config.AbsoluteInheritedDefaultBehaviorProvider; import org.forgerock.opendj.config.AbstractManagedObjectDefinition; import org.forgerock.opendj.config.AggregationPropertyDefinition; import org.forgerock.opendj.config.AliasDefaultBehaviorProvider; import org.forgerock.opendj.config.BooleanPropertyDefinition; import org.forgerock.opendj.config.Configuration; import org.forgerock.opendj.config.ConfigurationClient; import org.forgerock.opendj.config.DefaultBehaviorProviderVisitor; import org.forgerock.opendj.config.DefinedDefaultBehaviorProvider; import org.forgerock.opendj.config.DefinitionDecodingException; import org.forgerock.opendj.config.EnumPropertyDefinition; import org.forgerock.opendj.config.InstantiableRelationDefinition; import org.forgerock.opendj.config.ManagedObjectDefinition; import org.forgerock.opendj.config.ManagedObjectNotFoundException; import org.forgerock.opendj.config.ManagedObjectPath; import org.forgerock.opendj.config.PropertyDefinition; import org.forgerock.opendj.config.PropertyDefinitionUsageBuilder; import org.forgerock.opendj.config.PropertyDefinitionVisitor; import org.forgerock.opendj.config.PropertyException; import org.forgerock.opendj.config.PropertyOption; import org.forgerock.opendj.config.RelativeInheritedDefaultBehaviorProvider; import org.forgerock.opendj.config.UndefinedDefaultBehaviorProvider; import org.forgerock.opendj.config.client.ManagedObject; import org.forgerock.opendj.config.client.ManagedObjectDecodingException; import org.forgerock.opendj.config.client.ManagementContext; import org.forgerock.opendj.ldap.AuthorizationException; import org.forgerock.opendj.ldap.LdapException; import org.forgerock.util.Reject; import com.forgerock.opendj.cli.ClientException; import com.forgerock.opendj.cli.ConsoleApplication; import com.forgerock.opendj.cli.HelpCallback; import com.forgerock.opendj.cli.Menu; import com.forgerock.opendj.cli.MenuBuilder; import com.forgerock.opendj.cli.MenuCallback; import com.forgerock.opendj.cli.MenuResult; import com.forgerock.opendj.cli.ReturnCode; import com.forgerock.opendj.cli.TableBuilder; import com.forgerock.opendj.cli.TextTablePrinter; import static org.forgerock.opendj.config.dsconfig.DSConfig.*; /** * Common methods used for interactively editing properties. */ final class PropertyValueEditor { /** * A menu call-back which can be used to dynamically create new components when configuring aggregation based * properties. */ private final class CreateComponentCallback<C extends ConfigurationClient, S extends Configuration> implements MenuCallback<String> { /** The aggregation property definition. */ private final AggregationPropertyDefinition<C, S> pd; /** * Creates a new component create call-back for the provided aggregation property definition. */ private CreateComponentCallback(AggregationPropertyDefinition<C, S> pd) { this.pd = pd; } /** {@inheritDoc} */ @Override public MenuResult<String> invoke(ConsoleApplication app) throws ClientException { try { // First get the parent managed object. InstantiableRelationDefinition<?, ?> rd = pd.getRelationDefinition(); ManagedObjectPath<?, ?> path = pd.getParentPath(); LocalizableMessage ufn = rd.getUserFriendlyName(); ManagedObject<?> parent; try { parent = context.getManagedObject(path); } catch (AuthorizationException e) { LocalizableMessage msg = ERR_DSCFG_ERROR_CREATE_AUTHZ.get(ufn); throw new ClientException(ReturnCode.INSUFFICIENT_ACCESS_RIGHTS, msg); } catch (DefinitionDecodingException e) { LocalizableMessage pufn = path.getManagedObjectDefinition().getUserFriendlyName(); LocalizableMessage msg = ERR_DSCFG_ERROR_GET_PARENT_DDE.get(pufn, pufn, pufn); throw new ClientException(ReturnCode.OTHER, msg); } catch (ManagedObjectDecodingException e) { LocalizableMessage pufn = path.getManagedObjectDefinition().getUserFriendlyName(); LocalizableMessage msg = ERR_DSCFG_ERROR_GET_PARENT_MODE.get(pufn); throw new ClientException(ReturnCode.OTHER, msg, e); } catch (LdapException e) { LocalizableMessage msg = ERR_DSCFG_ERROR_CREATE_CE.get(ufn, e.getMessage()); throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, msg); } catch (ManagedObjectNotFoundException e) { LocalizableMessage pufn = path.getManagedObjectDefinition().getUserFriendlyName(); LocalizableMessage msg = ERR_DSCFG_ERROR_GET_PARENT_MONFE.get(pufn); return interactivePrintOrThrowError(app, msg, NO_SUCH_OBJECT); } // Now let the user create the child component. app.println(); app.println(); return CreateSubCommandHandler.createManagedObject(app, context, parent, rd); } catch (ClientException e) { // FIXME: should really do something better with the exception // handling here. For example, if a authz or communications // exception occurs then the application should exit. app.errPrintln(); app.errPrintln(e.getMessageObject()); app.errPrintln(); app.pressReturnToContinue(); return MenuResult.cancel(); } } } /** * A help call-back which displays a description and summary of a component and its properties. */ private static final class ComponentHelpCallback implements HelpCallback { /** The managed object being edited. */ private final ManagedObject<?> mo; /** The properties that can be edited. */ private final Collection<PropertyDefinition<?>> properties; /** Creates a new component helper for the specified property. */ private ComponentHelpCallback(ManagedObject<?> mo, Collection<PropertyDefinition<?>> c) { this.mo = mo; this.properties = c; } /** {@inheritDoc} */ @Override public void display(ConsoleApplication app) { app.println(); HelpSubCommandHandler.displaySingleComponent(app, mo, properties); app.println(); app.pressReturnToContinue(); } } /** * A simple interface for querying and retrieving common default behavior properties. */ private static final class DefaultBehaviorQuery<T> { /** * The type of default behavior. */ private enum Type { /** * Alias default behavior. */ ALIAS, /** * Defined default behavior. */ DEFINED, /** * Inherited default behavior. */ INHERITED, /** * Undefined default behavior. */ UNDEFINED; } /** * Create a new default behavior query object based on the provied property definition. * * @param <T> * The type of property definition. * @param pd * The property definition. * @return The default behavior query object. */ public static <T> DefaultBehaviorQuery<T> query(PropertyDefinition<T> pd) { DefaultBehaviorProviderVisitor<T, DefaultBehaviorQuery<T>, PropertyDefinition<T>> visitor = new DefaultBehaviorProviderVisitor<T, DefaultBehaviorQuery<T>, PropertyDefinition<T>>() { /** {@inheritDoc} */ @Override public DefaultBehaviorQuery<T> visitAbsoluteInherited(AbsoluteInheritedDefaultBehaviorProvider<T> d, PropertyDefinition<T> p) { AbstractManagedObjectDefinition<?, ?> mod = d.getManagedObjectDefinition(); String propertyName = d.getPropertyName(); PropertyDefinition<?> pd2 = mod.getPropertyDefinition(propertyName); DefaultBehaviorQuery<?> query = query(pd2); return new DefaultBehaviorQuery<>(Type.INHERITED, query.getAliasDescription()); } /** {@inheritDoc} */ @Override public DefaultBehaviorQuery<T> visitAlias(AliasDefaultBehaviorProvider<T> d, PropertyDefinition<T> p) { return new DefaultBehaviorQuery<>(Type.ALIAS, d.getSynopsis()); } /** {@inheritDoc} */ @Override public DefaultBehaviorQuery<T> visitDefined(DefinedDefaultBehaviorProvider<T> d, PropertyDefinition<T> p) { return new DefaultBehaviorQuery<>(Type.DEFINED, null); } /** {@inheritDoc} */ @Override public DefaultBehaviorQuery<T> visitRelativeInherited(RelativeInheritedDefaultBehaviorProvider<T> d, PropertyDefinition<T> p) { AbstractManagedObjectDefinition<?, ?> mod = d.getManagedObjectDefinition(); String propertyName = d.getPropertyName(); PropertyDefinition<?> pd2 = mod.getPropertyDefinition(propertyName); DefaultBehaviorQuery<?> query = query(pd2); return new DefaultBehaviorQuery<>(Type.INHERITED, query.getAliasDescription()); } /** {@inheritDoc} */ @Override public DefaultBehaviorQuery<T> visitUndefined(UndefinedDefaultBehaviorProvider<T> d, PropertyDefinition<T> p) { return new DefaultBehaviorQuery<>(Type.UNDEFINED, null); } }; return pd.getDefaultBehaviorProvider().accept(visitor, pd); } /** * The description of the behavior if it is an alias default behavior. */ private final LocalizableMessage aliasDescription; /** The type of behavior. */ private final Type type; /** Private constructor. */ private DefaultBehaviorQuery(Type type, LocalizableMessage aliasDescription) { this.type = type; this.aliasDescription = aliasDescription; } /** * Gets the detailed description of this default behavior if it is an alias default behavior or if it inherits * from an alias default behavior. * * @return Returns the detailed description of this default behavior if it is an alias default behavior or if it * inherits from an alias default behavior, otherwise <code>null</code>. */ public LocalizableMessage getAliasDescription() { return aliasDescription; } /** * Determines whether or not the default behavior is alias. * * @return Returns <code>true</code> if the default behavior is alias. */ public boolean isAlias() { return type == Type.ALIAS; } /** * Determines whether or not the default behavior is defined. * * @return Returns <code>true</code> if the default behavior is defined. */ public boolean isDefined() { return type == Type.DEFINED; } /** * Determines whether or not the default behavior is inherited. * * @return Returns <code>true</code> if the default behavior is inherited. */ public boolean isInherited() { return type == Type.INHERITED; } /** * Determines whether or not the default behavior is undefined. * * @return Returns <code>true</code> if the default behavior is undefined. */ public boolean isUndefined() { return type == Type.UNDEFINED; } } /** * A property definition visitor which initializes mandatory properties. */ private final class MandatoryPropertyInitializer extends PropertyDefinitionVisitor<MenuResult<Void>, Void> implements MenuCallback<Void> { /** Any exception that was caught during processing. */ private ClientException e; /** The managed object being edited. */ private final ManagedObject<?> mo; /** The property to be edited. */ private final PropertyDefinition<?> pd; /** Creates a new property editor for the specified property. */ private MandatoryPropertyInitializer(ManagedObject<?> mo, PropertyDefinition<?> pd) { this.mo = mo; this.pd = pd; } /** {@inheritDoc} */ @Override public MenuResult<Void> invoke(ConsoleApplication app) throws ClientException { displayPropertyHeader(app, pd); MenuResult<Void> result = pd.accept(this, null); if (e != null) { throw e; } return result; } /** {@inheritDoc} */ @Override public <C extends ConfigurationClient, S extends Configuration> MenuResult<Void> visitAggregation( AggregationPropertyDefinition<C, S> d, Void p) { MenuBuilder<String> builder = new MenuBuilder<>(app); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); InstantiableRelationDefinition<C, S> rd = d.getRelationDefinition(); if (d.hasOption(PropertyOption.MULTI_VALUED)) { builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_COMPONENT_MULTI.get(rd.getUserFriendlyPluralName(), d.getName())); builder.setAllowMultiSelect(true); } else { builder.setPrompt( INFO_EDITOR_PROMPT_SELECT_COMPONENT_SINGLE.get(rd.getUserFriendlyName(), d.getName())); } // Create a list of possible names. Set<String> values = new TreeSet<>(d); ManagedObjectPath<?, ?> path = d.getParentPath(); try { values.addAll(Arrays.asList(context.listManagedObjects(path, rd))); } catch (AuthorizationException e) { this.e = new ClientException( ReturnCode.CLIENT_SIDE_PARAM_ERROR, LocalizableMessage.raw(e.getMessage())); return MenuResult.quit(); } catch (ManagedObjectNotFoundException e) { this.e = new ClientException(ReturnCode.NO_SUCH_OBJECT, e.getMessageObject()); return MenuResult.cancel(); } catch (LdapException e) { this.e = new ClientException(ReturnCode.APPLICATION_ERROR, LocalizableMessage.raw(e.getMessage())); return MenuResult.quit(); } for (String value : values) { LocalizableMessage option = getPropertyValues(d, Collections.singleton(value)); builder.addNumberedOption(option, MenuResult.success(value)); } MenuCallback<String> callback = new CreateComponentCallback<>(d); builder.addNumberedOption( INFO_EDITOR_OPTION_CREATE_A_NEW_COMPONENT.get(rd.getUserFriendlyName()), callback); builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); if (app.isMenuDrivenMode()) { builder.addCancelOption(true); } builder.addQuitOption(); Menu<String> menu = builder.toMenu(); try { app.println(); MenuResult<String> result = menu.run(); if (result.isQuit()) { return MenuResult.quit(); } else if (result.isCancel()) { return MenuResult.cancel(); } else { Collection<String> newValues = result.getValues(); SortedSet<String> oldValues = new TreeSet<>(mo.getPropertyValues(d)); mo.setPropertyValues(d, newValues); isLastChoiceReset = false; registerModification(d, new TreeSet<String>(newValues), oldValues); return MenuResult.success(); } } catch (ClientException e) { this.e = e; return MenuResult.cancel(); } } /** {@inheritDoc} */ @Override public MenuResult<Void> visitBoolean(BooleanPropertyDefinition d, Void p) { MenuBuilder<Boolean> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUE_SINGLE.get(d.getName())); builder.addNumberedOption(INFO_VALUE_TRUE.get(), MenuResult.success(true)); builder.addNumberedOption(INFO_VALUE_FALSE.get(), MenuResult.success(false)); builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); if (app.isMenuDrivenMode()) { builder.addCancelOption(true); } builder.addQuitOption(); Menu<Boolean> menu = builder.toMenu(); try { app.println(); MenuResult<Boolean> result = menu.run(); if (result.isQuit()) { return MenuResult.quit(); } else if (result.isCancel()) { return MenuResult.cancel(); } else { Collection<Boolean> newValues = result.getValues(); SortedSet<Boolean> oldValues = new TreeSet<>(mo.getPropertyValues(d)); mo.setPropertyValues(d, newValues); isLastChoiceReset = false; registerModification(d, new TreeSet<Boolean>(newValues), oldValues); return MenuResult.success(); } } catch (ClientException e) { this.e = e; return MenuResult.cancel(); } } /** {@inheritDoc} */ @Override public <E extends Enum<E>> MenuResult<Void> visitEnum(EnumPropertyDefinition<E> d, Void x) { MenuBuilder<E> builder = new MenuBuilder<>(app); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); if (d.hasOption(PropertyOption.MULTI_VALUED)) { builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUE_MULTI.get(d.getName())); builder.setAllowMultiSelect(true); } else { builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUE_SINGLE.get(d.getName())); } Set<E> values = new TreeSet<>(d); values.addAll(EnumSet.allOf(d.getEnumClass())); for (E value : values) { LocalizableMessage option = getPropertyValues(d, Collections.singleton(value)); builder.addNumberedOption(option, MenuResult.success(value)); } builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); if (app.isMenuDrivenMode()) { builder.addCancelOption(true); } builder.addQuitOption(); Menu<E> menu = builder.toMenu(); try { app.println(); MenuResult<E> result = menu.run(); if (result.isQuit()) { return MenuResult.quit(); } else if (result.isCancel()) { return MenuResult.cancel(); } else { Collection<E> newValues = result.getValues(); SortedSet<E> oldValues = new TreeSet<>(mo.getPropertyValues(d)); mo.setPropertyValues(d, newValues); isLastChoiceReset = false; registerModification(d, new TreeSet<E>(newValues), oldValues); return MenuResult.success(); } } catch (ClientException e) { this.e = e; return MenuResult.cancel(); } } /** {@inheritDoc} */ @Override public <T> MenuResult<Void> visitUnknown(PropertyDefinition<T> d, Void p) { app.println(); displayPropertySyntax(app, d); // Set the new property value(s). try { SortedSet<T> values = readPropertyValues(app, mo.getManagedObjectDefinition(), d); SortedSet<T> oldValues = new TreeSet<>(mo.getPropertyValues(d)); mo.setPropertyValues(d, values); isLastChoiceReset = false; registerModification(d, values, oldValues); return MenuResult.success(); } catch (ClientException e) { this.e = e; return MenuResult.cancel(); } } } /** * A menu call-back for editing a modifiable multi-valued property. */ private final class MultiValuedPropertyEditor extends PropertyDefinitionVisitor<MenuResult<Boolean>, Void> implements MenuCallback<Boolean> { /** Any exception that was caught during processing. */ private ClientException e; /** The managed object being edited. */ private final ManagedObject<?> mo; /** The property to be edited. */ private final PropertyDefinition<?> pd; /** Creates a new property editor for the specified property. */ private MultiValuedPropertyEditor(ManagedObject<?> mo, PropertyDefinition<?> pd) { Reject.ifFalse(pd.hasOption(PropertyOption.MULTI_VALUED)); this.mo = mo; this.pd = pd; } /** {@inheritDoc} */ @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { displayPropertyHeader(app, pd); MenuResult<Boolean> result = pd.accept(this, null); if (e != null) { throw e; } return result; } /** {@inheritDoc} */ @Override public <C extends ConfigurationClient, S extends Configuration> MenuResult<Boolean> visitAggregation( final AggregationPropertyDefinition<C, S> d, Void p) { final SortedSet<String> defaultValues = mo.getPropertyDefaultValues(d); final SortedSet<String> oldValues = mo.getPropertyValues(d); final SortedSet<String> currentValues = mo.getPropertyValues(d); final InstantiableRelationDefinition<C, S> rd = d.getRelationDefinition(); final LocalizableMessage ufpn = rd.getUserFriendlyPluralName(); boolean isFirst = true; while (true) { if (!isFirst) { app.println(); app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY_CONT.get(d.getName())); } else { isFirst = false; } if (currentValues.size() > 1) { app.println(); app.println(INFO_EDITOR_HEADING_COMPONENT_SUMMARY.get(d.getName(), ufpn)); app.println(); displayPropertyValues(app, d, currentValues); } // Create a list of possible names. final Set<String> values = new TreeSet<>(d); ManagedObjectPath<?, ?> path = d.getParentPath(); try { values.addAll(Arrays.asList(context.listManagedObjects(path, rd))); } catch (AuthorizationException e) { this.e = new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, LocalizableMessage.raw(e .getMessage())); return MenuResult.quit(); } catch (ManagedObjectNotFoundException e) { this.e = new ClientException(ReturnCode.NO_SUCH_OBJECT, e.getMessageObject()); return MenuResult.cancel(); } catch (LdapException e) { this.e = new ClientException(ReturnCode.APPLICATION_ERROR, LocalizableMessage.raw(e.getMessage())); return MenuResult.quit(); } // Create the add values call-back. MenuCallback<Boolean> addCallback = null; values.removeAll(currentValues); if (!values.isEmpty()) { addCallback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { MenuBuilder<String> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_COMPONENTS_ADD.get(ufpn)); builder.setAllowMultiSelect(true); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); for (String value : values) { LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value)); builder.addNumberedOption(svalue, MenuResult.success(value)); } MenuCallback<String> callback = new CreateComponentCallback<>(d); builder.addNumberedOption( INFO_EDITOR_OPTION_CREATE_A_NEW_COMPONENT.get(rd.getUserFriendlyName()), callback); if (values.size() > 1) { // No point in having this option if there's only one // possible value. builder.addNumberedOption(INFO_EDITOR_OPTION_ADD_ALL_COMPONENTS.get(ufpn), MenuResult.success(values)); } builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); builder.addCancelOption(true); builder.addQuitOption(); app.println(); app.println(); Menu<String> menu = builder.toMenu(); MenuResult<String> result = menu.run(); if (result.isSuccess()) { // Set the new property value(s). Collection<String> addedValues = result.getValues(); currentValues.addAll(addedValues); isLastChoiceReset = false; app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else if (result.isCancel()) { app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { return MenuResult.quit(); } } }; } // Create the remove values call-back. MenuCallback<Boolean> removeCallback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { MenuBuilder<String> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_COMPONENTS_REMOVE.get(ufpn)); builder.setAllowMultiSelect(true); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); for (String value : currentValues) { LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value)); builder.addNumberedOption(svalue, MenuResult.success(value)); } builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); builder.addCancelOption(true); builder.addQuitOption(); app.println(); app.println(); Menu<String> menu = builder.toMenu(); MenuResult<String> result = menu.run(); if (result.isSuccess()) { // Set the new property value(s). Collection<String> removedValues = result.getValues(); currentValues.removeAll(removedValues); isLastChoiceReset = false; app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else if (result.isCancel()) { app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { return MenuResult.quit(); } } }; MenuResult<Boolean> result = runMenu(d, app, defaultValues, oldValues, currentValues, addCallback, removeCallback); if (!result.isAgain()) { return result; } } } /** {@inheritDoc} */ @Override public <T extends Enum<T>> MenuResult<Boolean> visitEnum(final EnumPropertyDefinition<T> d, Void p) { final SortedSet<T> defaultValues = mo.getPropertyDefaultValues(d); final SortedSet<T> oldValues = mo.getPropertyValues(d); final SortedSet<T> currentValues = mo.getPropertyValues(d); boolean isFirst = true; while (true) { if (!isFirst) { app.println(); app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY_CONT.get(d.getName())); } else { isFirst = false; } if (currentValues.size() > 1) { app.println(); app.println(INFO_EDITOR_HEADING_VALUES_SUMMARY.get(d.getName())); app.println(); displayPropertyValues(app, d, currentValues); } // Create the add values call-back. MenuCallback<Boolean> addCallback = null; final EnumSet<T> values = EnumSet.allOf(d.getEnumClass()); values.removeAll(currentValues); if (!values.isEmpty()) { addCallback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { MenuBuilder<T> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUES_ADD.get()); builder.setAllowMultiSelect(true); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); for (T value : values) { LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value)); builder.addNumberedOption(svalue, MenuResult.success(value)); } if (values.size() > 1) { // No point in having this option if there's only one // possible value. builder.addNumberedOption(INFO_EDITOR_OPTION_ADD_ALL_VALUES.get(), MenuResult.success(values)); } builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); builder.addCancelOption(true); builder.addQuitOption(); app.println(); app.println(); Menu<T> menu = builder.toMenu(); MenuResult<T> result = menu.run(); if (result.isSuccess()) { // Set the new property value(s). Collection<T> addedValues = result.getValues(); currentValues.addAll(addedValues); isLastChoiceReset = false; app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else if (result.isCancel()) { app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { return MenuResult.quit(); } } }; } // Create the remove values call-back. MenuCallback<Boolean> removeCallback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { MenuBuilder<T> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUES_REMOVE.get()); builder.setAllowMultiSelect(true); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); for (T value : currentValues) { LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value)); builder.addNumberedOption(svalue, MenuResult.success(value)); } builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); builder.addCancelOption(true); builder.addQuitOption(); app.println(); app.println(); Menu<T> menu = builder.toMenu(); MenuResult<T> result = menu.run(); if (result.isSuccess()) { // Set the new property value(s). Collection<T> removedValues = result.getValues(); currentValues.removeAll(removedValues); isLastChoiceReset = false; app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else if (result.isCancel()) { app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { return MenuResult.quit(); } } }; MenuResult<Boolean> result = runMenu(d, app, defaultValues, oldValues, currentValues, addCallback, removeCallback); if (!result.isAgain()) { return result; } } } /** {@inheritDoc} */ @Override public <T> MenuResult<Boolean> visitUnknown(final PropertyDefinition<T> d, Void p) { app.println(); displayPropertySyntax(app, d); final SortedSet<T> defaultValues = mo.getPropertyDefaultValues(d); final SortedSet<T> oldValues = mo.getPropertyValues(d); final SortedSet<T> currentValues = mo.getPropertyValues(d); boolean isFirst = true; while (true) { if (!isFirst) { app.println(); app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY_CONT.get(d.getName())); } else { isFirst = false; } if (currentValues.size() > 1) { app.println(); app.println(INFO_EDITOR_HEADING_VALUES_SUMMARY.get(d.getName())); app.println(); displayPropertyValues(app, d, currentValues); } // Create the add values call-back. MenuCallback<Boolean> addCallback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { app.println(); SortedSet<T> previousValues = new TreeSet<>(currentValues); readPropertyValues(app, mo.getManagedObjectDefinition(), d, currentValues); SortedSet<T> addedValues = new TreeSet<>(currentValues); addedValues.removeAll(previousValues); isLastChoiceReset = false; return MenuResult.success(false); } }; // Create the remove values call-back. MenuCallback<Boolean> removeCallback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { MenuBuilder<T> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUES_REMOVE.get()); builder.setAllowMultiSelect(true); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); for (T value : currentValues) { LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value)); builder.addNumberedOption(svalue, MenuResult.success(value)); } builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); builder.addCancelOption(true); builder.addQuitOption(); app.println(); app.println(); Menu<T> menu = builder.toMenu(); MenuResult<T> result = menu.run(); if (result.isSuccess()) { // Set the new property value(s). Collection<T> removedValues = result.getValues(); currentValues.removeAll(removedValues); isLastChoiceReset = false; app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else if (result.isCancel()) { app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { return MenuResult.quit(); } } }; MenuResult<Boolean> result = runMenu(d, app, defaultValues, oldValues, currentValues, addCallback, removeCallback); if (!result.isAgain()) { return result; } } } /** * Generate an appropriate menu option for a property which asks the user whether or not they want to keep the * property's current settings. */ private <T> LocalizableMessage getKeepDefaultValuesMenuOption(PropertyDefinition<T> pd, SortedSet<T> defaultValues, SortedSet<T> oldValues, SortedSet<T> currentValues) { DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd); boolean isModified = !currentValues.equals(oldValues); boolean isDefault = currentValues.equals(defaultValues); if (isModified) { switch (currentValues.size()) { case 0: if (query.isAlias()) { return INFO_EDITOR_OPTION_USE_DEFAULT_ALIAS.get(query.getAliasDescription()); } else if (query.isInherited()) { if (query.getAliasDescription() != null) { return INFO_EDITOR_OPTION_USE_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription()); } else { return INFO_EDITOR_OPTION_USE_DEFAULT_INHERITED_ALIAS_UNDEFINED.get(); } } else { return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get(); } case 1: LocalizableMessage svalue = getPropertyValues(pd, currentValues); if (isDefault) { if (query.isInherited()) { return INFO_EDITOR_OPTION_USE_INHERITED_DEFAULT_VALUE.get(svalue); } else { return INFO_EDITOR_OPTION_USE_DEFAULT_VALUE.get(svalue); } } else { return INFO_EDITOR_OPTION_USE_VALUE.get(svalue); } default: if (isDefault) { if (query.isInherited()) { return INFO_EDITOR_OPTION_USE_INHERITED_DEFAULT_VALUES.get(); } else { return INFO_EDITOR_OPTION_USE_DEFAULT_VALUES.get(); } } else { return INFO_EDITOR_OPTION_USE_VALUES.get(); } } } else { switch (currentValues.size()) { case 0: if (query.isAlias()) { return INFO_EDITOR_OPTION_KEEP_DEFAULT_ALIAS.get(query.getAliasDescription()); } else if (query.isInherited()) { if (query.getAliasDescription() != null) { return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription()); } else { return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS_UNDEFINED.get(); } } else { return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get(); } case 1: LocalizableMessage svalue = getPropertyValues(pd, currentValues); if (isDefault) { if (query.isInherited()) { return INFO_EDITOR_OPTION_KEEP_INHERITED_DEFAULT_VALUE.get(svalue); } else { return INFO_EDITOR_OPTION_KEEP_DEFAULT_VALUE.get(svalue); } } else { return INFO_EDITOR_OPTION_KEEP_VALUE.get(svalue); } default: if (isDefault) { if (query.isInherited()) { return INFO_EDITOR_OPTION_KEEP_INHERITED_DEFAULT_VALUES.get(); } else { return INFO_EDITOR_OPTION_KEEP_DEFAULT_VALUES.get(); } } else { return INFO_EDITOR_OPTION_KEEP_VALUES.get(); } } } } /** * Generate an appropriate menu option which should be used in the case where a property can be reset to its * default behavior. */ private <T> LocalizableMessage getResetToDefaultValuesMenuOption(PropertyDefinition<T> pd, SortedSet<T> defaultValues, SortedSet<T> currentValues) { DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd); boolean isMandatory = pd.hasOption(PropertyOption.MANDATORY); if (!isMandatory && query.isAlias()) { return INFO_EDITOR_OPTION_RESET_DEFAULT_ALIAS.get(query.getAliasDescription()); } else if (query.isDefined()) { // Only show this option if the current value is different // to the default. if (!currentValues.equals(defaultValues)) { LocalizableMessage svalue = getPropertyValues(pd, defaultValues); if (defaultValues.size() > 1) { return INFO_EDITOR_OPTION_RESET_DEFAULT_VALUES.get(svalue); } else { return INFO_EDITOR_OPTION_RESET_DEFAULT_VALUE.get(svalue); } } else { return null; } } else if (!isMandatory && query.isInherited()) { if (defaultValues.isEmpty()) { if (query.getAliasDescription() != null) { return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription()); } else { return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS_UNDEFINED.get(); } } else { LocalizableMessage svalue = getPropertyValues(pd, defaultValues); if (defaultValues.size() > 1) { return INFO_EDITOR_OPTION_RESET_INHERITED_DEFAULT_VALUES.get(svalue); } else { return INFO_EDITOR_OPTION_RESET_INHERITED_DEFAULT_VALUE.get(svalue); } } } else if (!isMandatory && query.isUndefined()) { return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get(); } else { return null; } } /** Common menu processing. */ private <T> MenuResult<Boolean> runMenu(final PropertyDefinition<T> d, ConsoleApplication app, final SortedSet<T> defaultValues, final SortedSet<T> oldValues, final SortedSet<T> currentValues, MenuCallback<Boolean> addCallback, MenuCallback<Boolean> removeCallback) { // Construct a menu of actions. MenuBuilder<Boolean> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName())); // First option is for leaving the property unchanged or // applying changes, but only if the state of the property is valid. if (!d.hasOption(PropertyOption.MANDATORY) || !currentValues.isEmpty()) { MenuResult<Boolean> result; if (!oldValues.equals(currentValues)) { result = MenuResult.success(true); } else { result = MenuResult.cancel(); } LocalizableMessage option = getKeepDefaultValuesMenuOption(d, defaultValues, oldValues, currentValues); builder.addNumberedOption(option, result); builder.setDefault(LocalizableMessage.raw("1"), result); } // Add an option for adding some values. if (addCallback != null) { int i = builder.addNumberedOption(INFO_EDITOR_OPTION_ADD_ONE_OR_MORE_VALUES.get(), addCallback); if (d.hasOption(PropertyOption.MANDATORY) && currentValues.isEmpty()) { builder.setDefault(LocalizableMessage.raw("%d", i), addCallback); } } // Add options for removing values if applicable. if (!currentValues.isEmpty()) { builder.addNumberedOption(INFO_EDITOR_OPTION_REMOVE_ONE_OR_MORE_VALUES.get(), removeCallback); } // Add options for removing all values and for resetting the // property to its default behavior. LocalizableMessage resetOption = null; if (!currentValues.equals(defaultValues)) { resetOption = getResetToDefaultValuesMenuOption(d, defaultValues, currentValues); } if (!currentValues.isEmpty() && (resetOption == null || !defaultValues.isEmpty())) { MenuCallback<Boolean> callback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { isLastChoiceReset = false; currentValues.clear(); app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } }; builder.addNumberedOption(INFO_EDITOR_OPTION_REMOVE_ALL_VALUES.get(), callback); } if (resetOption != null) { MenuCallback<Boolean> callback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { currentValues.clear(); currentValues.addAll(defaultValues); isLastChoiceReset = true; app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } }; builder.addNumberedOption(resetOption, callback); } // Add an option for undoing any changes. if (!oldValues.equals(currentValues)) { MenuCallback<Boolean> callback = new MenuCallback<Boolean>() { @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { currentValues.clear(); currentValues.addAll(oldValues); isLastChoiceReset = false; app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } }; builder.addNumberedOption(INFO_EDITOR_OPTION_REVERT_CHANGES.get(), callback); } builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); builder.addQuitOption(); Menu<Boolean> menu = builder.toMenu(); MenuResult<Boolean> result; try { app.println(); result = menu.run(); } catch (ClientException e) { this.e = e; return null; } if (result.isSuccess()) { if (result.getValue()) { // Set the new property value(s). mo.setPropertyValues(d, currentValues); registerModification(d, currentValues, oldValues); app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { // Continue until cancel/apply changes. app.println(); return MenuResult.again(); } } else if (result.isCancel()) { app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { return MenuResult.quit(); } } } /** * A help call-back which displays a description and summary of a single property. */ private static final class PropertyHelpCallback implements HelpCallback { /** The managed object definition. */ private final ManagedObjectDefinition<?, ?> d; /** The property to be edited. */ private final PropertyDefinition<?> pd; /** Creates a new property helper for the specified property. */ private PropertyHelpCallback(ManagedObjectDefinition<?, ?> d, PropertyDefinition<?> pd) { this.d = d; this.pd = pd; } /** {@inheritDoc} */ @Override public void display(ConsoleApplication app) { app.println(); HelpSubCommandHandler.displayVerboseSingleProperty(app, d, pd.getName()); app.println(); app.pressReturnToContinue(); } } /** * A menu call-back for viewing a read-only properties. */ private final class ReadOnlyPropertyViewer extends PropertyDefinitionVisitor<MenuResult<Boolean>, Void> implements MenuCallback<Boolean> { /** Any exception that was caught during processing. */ private ClientException e; /** The managed object being edited. */ private final ManagedObject<?> mo; /** The property to be edited. */ private final PropertyDefinition<?> pd; /** Creates a new property editor for the specified property. */ private ReadOnlyPropertyViewer(ManagedObject<?> mo, PropertyDefinition<?> pd) { this.mo = mo; this.pd = pd; } /** {@inheritDoc} */ @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { MenuResult<Boolean> result = pd.accept(this, null); if (e != null) { throw e; } return result; } /** {@inheritDoc} */ @Override public <T> MenuResult<Boolean> visitUnknown(PropertyDefinition<T> pd, Void p) { SortedSet<T> values = mo.getPropertyValues(pd); app.println(); app.println(); switch (values.size()) { case 0: // Only alias, undefined, or inherited alias or undefined // properties should apply here. DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd); LocalizableMessage aliasDescription = query.getAliasDescription(); if (aliasDescription != null) { app.println(INFO_EDITOR_HEADING_READ_ONLY_ALIAS.get(pd.getName(), aliasDescription)); } else { app.println(INFO_EDITOR_HEADING_READ_ONLY_ALIAS_UNDEFINED.get(pd.getName())); } break; case 1: LocalizableMessage svalue = getPropertyValues(pd, mo); app.println(INFO_EDITOR_HEADING_READ_ONLY_VALUE.get(pd.getName(), svalue)); break; default: app.println(INFO_EDITOR_HEADING_READ_ONLY_VALUES.get(pd.getName())); app.println(); displayPropertyValues(app, pd, values); break; } app.println(); boolean result; try { result = app.confirmAction(INFO_EDITOR_PROMPT_READ_ONLY.get(), false); } catch (ClientException e) { this.e = e; return null; } if (result) { app.println(); HelpSubCommandHandler.displayVerboseSingleProperty(app, mo.getManagedObjectDefinition(), pd.getName()); app.println(); app.pressReturnToContinue(); } return MenuResult.again(); } } /** * A menu call-back for editing a modifiable single-valued property. */ private final class SingleValuedPropertyEditor extends PropertyDefinitionVisitor<MenuResult<Boolean>, Void> implements MenuCallback<Boolean> { /** Any exception that was caught during processing. */ private ClientException e; /** The managed object being edited. */ private final ManagedObject<?> mo; /** The property to be edited. */ private final PropertyDefinition<?> pd; /** Creates a new property editor for the specified property. */ private SingleValuedPropertyEditor(ManagedObject<?> mo, PropertyDefinition<?> pd) { Reject.ifFalse(!pd.hasOption(PropertyOption.MULTI_VALUED)); this.mo = mo; this.pd = pd; } /** {@inheritDoc} */ @Override public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException { displayPropertyHeader(app, pd); MenuResult<Boolean> result = pd.accept(this, null); if (e != null) { throw e; } return result; } /** {@inheritDoc} */ @Override public <C extends ConfigurationClient, S extends Configuration> MenuResult<Boolean> visitAggregation( AggregationPropertyDefinition<C, S> d, Void p) { // Construct a menu of actions. MenuBuilder<String> builder = new MenuBuilder<>(app); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName())); DefaultBehaviorQuery<String> query = DefaultBehaviorQuery.query(d); SortedSet<String> currentValues = mo.getPropertyValues(d); SortedSet<String> defaultValues = mo.getPropertyDefaultValues(d); String currentValue = currentValues.isEmpty() ? null : currentValues.first(); String defaultValue = defaultValues.isEmpty() ? null : defaultValues.first(); // First option is for leaving the property unchanged. LocalizableMessage option = getKeepDefaultValuesMenuOption(d); builder.addNumberedOption(option, MenuResult.<String> cancel()); builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<String> cancel()); // Create a list of possible names. final Set<String> values = new TreeSet<>(d); ManagedObjectPath<?, ?> path = d.getParentPath(); InstantiableRelationDefinition<C, S> rd = d.getRelationDefinition(); try { values.addAll(Arrays.asList(context.listManagedObjects(path, rd))); } catch (AuthorizationException e) { this.e = new ClientException( ReturnCode.CLIENT_SIDE_PARAM_ERROR, LocalizableMessage.raw(e.getMessage())); return MenuResult.quit(); } catch (ManagedObjectNotFoundException e) { this.e = new ClientException(ReturnCode.NO_SUCH_OBJECT, e.getMessageObject()); return MenuResult.cancel(); } catch (LdapException e) { this.e = new ClientException(ReturnCode.APPLICATION_ERROR, LocalizableMessage.raw(e.getMessage())); return MenuResult.quit(); } final LocalizableMessage ufn = rd.getUserFriendlyName(); for (String value : values) { if (currentValue != null && d.compare(value, currentValue) == 0) { // This option is unnecessary. continue; } LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value)); if (value.equals(defaultValue) && query.isDefined()) { option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_COMPONENT.get(ufn, svalue); } else { option = INFO_EDITOR_OPTION_CHANGE_TO_COMPONENT.get(ufn, svalue); } builder.addNumberedOption(option, MenuResult.success(value)); } MenuCallback<String> callback = new CreateComponentCallback<>(d); builder.addNumberedOption(INFO_EDITOR_OPTION_CREATE_A_NEW_COMPONENT.get(ufn), callback); // Third option is to reset the value back to its default. if (mo.isPropertyPresent(d) && !query.isDefined()) { option = getResetToDefaultValuesMenuOption(d); if (option != null) { builder.addNumberedOption(option, MenuResult.<String> success()); } } return runMenu(d, builder); } /** {@inheritDoc} */ @Override public MenuResult<Boolean> visitBoolean(BooleanPropertyDefinition d, Void p) { // Construct a menu of actions. MenuBuilder<Boolean> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName())); DefaultBehaviorQuery<Boolean> query = DefaultBehaviorQuery.query(d); SortedSet<Boolean> currentValues = mo.getPropertyValues(d); SortedSet<Boolean> defaultValues = mo.getPropertyDefaultValues(d); Boolean currentValue = currentValues.isEmpty() ? null : currentValues.first(); Boolean defaultValue = defaultValues.isEmpty() ? null : defaultValues.first(); // First option is for leaving the property unchanged. LocalizableMessage option = getKeepDefaultValuesMenuOption(d); builder.addNumberedOption(option, MenuResult.<Boolean> cancel()); builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<Boolean> cancel()); // The second (and possibly third) option is to always change // the property's value. if (!Boolean.TRUE.equals(currentValue)) { LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(true)); if (Boolean.TRUE.equals(defaultValue)) { option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_VALUE.get(svalue); } else { option = INFO_EDITOR_OPTION_CHANGE_TO_VALUE.get(svalue); } builder.addNumberedOption(option, MenuResult.success(true)); } if (!Boolean.FALSE.equals(currentValue)) { LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(false)); if (Boolean.FALSE.equals(defaultValue)) { option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_VALUE.get(svalue); } else { option = INFO_EDITOR_OPTION_CHANGE_TO_VALUE.get(svalue); } builder.addNumberedOption(option, MenuResult.success(false)); } // Final option is to reset the value back to its default. if (mo.isPropertyPresent(d) && !query.isDefined()) { option = getResetToDefaultValuesMenuOption(d); if (option != null) { builder.addNumberedOption(option, MenuResult.<Boolean> success()); } } return runMenu(d, builder); } /** {@inheritDoc} */ @Override public <E extends Enum<E>> MenuResult<Boolean> visitEnum(EnumPropertyDefinition<E> d, Void p) { // Construct a menu of actions. MenuBuilder<E> builder = new MenuBuilder<>(app); builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD); builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName())); DefaultBehaviorQuery<E> query = DefaultBehaviorQuery.query(d); SortedSet<E> currentValues = mo.getPropertyValues(d); SortedSet<E> defaultValues = mo.getPropertyDefaultValues(d); E currentValue = currentValues.isEmpty() ? null : currentValues.first(); E defaultValue = defaultValues.isEmpty() ? null : defaultValues.first(); // First option is for leaving the property unchanged. LocalizableMessage option = getKeepDefaultValuesMenuOption(d); builder.addNumberedOption(option, MenuResult.<E> cancel()); builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<E> cancel()); // Create options for changing to other values. Set<E> values = new TreeSet<>(d); values.addAll(EnumSet.allOf(d.getEnumClass())); for (E value : values) { if (value.equals(currentValue) && query.isDefined()) { // This option is unnecessary. continue; } LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value)); if (value.equals(defaultValue) && query.isDefined()) { option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_VALUE.get(svalue); } else { option = INFO_EDITOR_OPTION_CHANGE_TO_VALUE.get(svalue); } builder.addNumberedOption(option, MenuResult.success(value)); } // Third option is to reset the value back to its default. if (mo.isPropertyPresent(d) && !query.isDefined()) { option = getResetToDefaultValuesMenuOption(d); if (option != null) { builder.addNumberedOption(option, MenuResult.<E> success()); } } return runMenu(d, builder); } /** {@inheritDoc} */ @Override public <T> MenuResult<Boolean> visitUnknown(final PropertyDefinition<T> d, Void p) { app.println(); displayPropertySyntax(app, d); // Construct a menu of actions. MenuBuilder<T> builder = new MenuBuilder<>(app); builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName())); // First option is for leaving the property unchanged. LocalizableMessage option = getKeepDefaultValuesMenuOption(d); builder.addNumberedOption(option, MenuResult.<T> cancel()); builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<T> cancel()); // The second option is to always change the property's value. builder.addNumberedOption(INFO_EDITOR_OPTION_CHANGE_VALUE.get(), new MenuCallback<T>() { @Override public MenuResult<T> invoke(ConsoleApplication app) throws ClientException { app.println(); Set<T> values = readPropertyValues(app, mo.getManagedObjectDefinition(), d); return MenuResult.success(values); } }); // Third option is to reset the value back to its default. if (mo.isPropertyPresent(d)) { option = getResetToDefaultValuesMenuOption(d); if (option != null) { builder.addNumberedOption(option, MenuResult.<T> success()); } } return runMenu(d, builder); } /** * Generate an appropriate menu option for a property which asks the user whether or not they want to keep the * property's current settings. */ private <T> LocalizableMessage getKeepDefaultValuesMenuOption(PropertyDefinition<T> pd) { DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd); SortedSet<T> currentValues = mo.getPropertyValues(pd); SortedSet<T> defaultValues = mo.getPropertyDefaultValues(pd); if (query.isDefined() && currentValues.equals(defaultValues)) { LocalizableMessage svalue = getPropertyValues(pd, currentValues); return INFO_EDITOR_OPTION_KEEP_DEFAULT_VALUE.get(svalue); } else if (mo.isPropertyPresent(pd)) { LocalizableMessage svalue = getPropertyValues(pd, currentValues); return INFO_EDITOR_OPTION_KEEP_VALUE.get(svalue); } else if (query.isAlias()) { return INFO_EDITOR_OPTION_KEEP_DEFAULT_ALIAS.get(query.getAliasDescription()); } else if (query.isInherited()) { if (defaultValues.isEmpty()) { if (query.getAliasDescription() != null) { return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription()); } else { return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS_UNDEFINED.get(); } } else { LocalizableMessage svalue = getPropertyValues(pd, defaultValues); return INFO_EDITOR_OPTION_KEEP_INHERITED_DEFAULT_VALUE.get(svalue); } } else { return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get(); } } /** * Generate an appropriate menu option which should be used in the case where a property can be reset to its * default behavior. */ private <T> LocalizableMessage getResetToDefaultValuesMenuOption(PropertyDefinition<T> pd) { DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd); SortedSet<T> currentValues = mo.getPropertyValues(pd); SortedSet<T> defaultValues = mo.getPropertyDefaultValues(pd); boolean isMandatory = pd.hasOption(PropertyOption.MANDATORY); if (!isMandatory && query.isAlias()) { return INFO_EDITOR_OPTION_RESET_DEFAULT_ALIAS.get(query.getAliasDescription()); } else if (query.isDefined()) { // Only show this option if the current value is different // to the default. if (!currentValues.equals(defaultValues)) { LocalizableMessage svalue = getPropertyValues(pd, defaultValues); return INFO_EDITOR_OPTION_RESET_DEFAULT_VALUE.get(svalue); } return null; } else if (!isMandatory && query.isInherited()) { if (defaultValues.isEmpty()) { if (query.getAliasDescription() != null) { return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription()); } else { return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS_UNDEFINED.get(); } } else { LocalizableMessage svalue = getPropertyValues(pd, defaultValues); return INFO_EDITOR_OPTION_RESET_INHERITED_DEFAULT_VALUE.get(svalue); } } else if (!isMandatory && query.isUndefined()) { return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get(); } return null; } /** Common menu processing. */ private <T> MenuResult<Boolean> runMenu(final PropertyDefinition<T> d, MenuBuilder<T> builder) { builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d)); builder.addQuitOption(); Menu<T> menu = builder.toMenu(); MenuResult<T> result; try { app.println(); result = menu.run(); } catch (ClientException e) { this.e = e; return null; } if (result.isSuccess()) { // Set the new property value(s). Collection<T> values = result.getValues(); // Both newValues and oldValues sets need to use the PropertyDefinition // as their comparator. Constructing a TreeSet<T> directly with the // values collection will fail if the values are e.g. InetAddresses. SortedSet<T> newValues = new TreeSet<>(d); newValues.addAll(values); SortedSet<T> oldValues = new TreeSet<>(mo.getPropertyValues(d)); mo.setPropertyValues(d, values); // If there are no values when we do a reset. isLastChoiceReset = values.isEmpty(); registerModification(d, newValues, oldValues); app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else if (result.isCancel()) { app.println(); app.pressReturnToContinue(); return MenuResult.success(false); } else { return MenuResult.quit(); } } } /** Display a title and a description of the property. */ private static void displayPropertyHeader(ConsoleApplication app, PropertyDefinition<?> pd) { app.println(); app.println(); app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY.get(pd.getName())); app.println(); app.println(pd.getSynopsis(), 4); if (pd.getDescription() != null) { app.println(); app.println(pd.getDescription(), 4); } } /** Display a property's syntax. */ private static <T> void displayPropertySyntax(ConsoleApplication app, PropertyDefinition<T> d) { PropertyDefinitionUsageBuilder b = new PropertyDefinitionUsageBuilder(true); TableBuilder builder = new TableBuilder(); builder.startRow(); builder.appendCell(INFO_EDITOR_HEADING_SYNTAX.get()); builder.appendCell(b.getUsage(d)); TextTablePrinter printer = new TextTablePrinter(app.getErrorStream()); printer.setDisplayHeadings(false); printer.setIndentWidth(4); printer.setColumnWidth(1, 0); builder.print(printer); } /** Display a table of property values. */ private static <T> void displayPropertyValues(ConsoleApplication app, PropertyDefinition<T> pd, Collection<T> values) { TableBuilder builder = new TableBuilder(); PropertyValuePrinter valuePrinter = new PropertyValuePrinter(null, null, false); int sz = values.size(); boolean useMultipleColumns = (sz >= MULTI_COLUMN_THRESHOLD); int rows = sz; if (useMultipleColumns) { // Display in two columns the first column should contain // half the values. If there are an odd number of columns // then the first column should contain an additional value // (e.g. if there are 23 values, the first column should // contain 12 values and the second column 11 values). rows /= 2; rows += sz % 2; } List<T> vl = new ArrayList<>(values); for (int i = 0, j = rows; i < rows; i++, j++) { builder.startRow(); builder.appendCell("*)"); builder.appendCell(valuePrinter.print(pd, vl.get(i))); if (useMultipleColumns && j < sz) { builder.appendCell(); builder.appendCell("*)"); builder.appendCell(valuePrinter.print(pd, vl.get(j))); } } TextTablePrinter printer = new TextTablePrinter(app.getErrorStream()); printer.setDisplayHeadings(false); printer.setIndentWidth(4); printer.setColumnWidth(1, 0); if (useMultipleColumns) { printer.setColumnWidth(2, 2); printer.setColumnWidth(4, 0); } builder.print(printer); } /** Display the set of values associated with a property. */ private static <T> LocalizableMessage getPropertyValues(PropertyDefinition<T> pd, Collection<T> values) { if (values.isEmpty()) { // There are no values or default values. Display the default // behavior for alias values. DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd); LocalizableMessage content = query.getAliasDescription(); if (content != null) { return content; } return LocalizableMessage.raw("-"); } else { PropertyValuePrinter printer = new PropertyValuePrinter(null, null, false); LocalizableMessageBuilder builder = new LocalizableMessageBuilder(); boolean isFirst = true; for (T value : values) { if (!isFirst) { builder.append(", "); } builder.append(printer.print(pd, value)); isFirst = false; } return builder.toMessage(); } } /** Display the set of values associated with a property. */ private static <T> LocalizableMessage getPropertyValues(PropertyDefinition<T> pd, ManagedObject<?> mo) { SortedSet<T> values = mo.getPropertyValues(pd); return getPropertyValues(pd, values); } /** Read new values for a property. */ private static <T> SortedSet<T> readPropertyValues(ConsoleApplication app, ManagedObjectDefinition<?, ?> d, PropertyDefinition<T> pd) throws ClientException { SortedSet<T> values = new TreeSet<>(pd); readPropertyValues(app, d, pd, values); return values; } /** Add values to a property. */ private static <T> void readPropertyValues(ConsoleApplication app, ManagedObjectDefinition<?, ?> d, PropertyDefinition<T> pd, SortedSet<T> values) throws ClientException { // Make sure there is at least one value if mandatory and empty. if (values.isEmpty()) { while (true) { try { LocalizableMessage prompt; if (pd.hasOption(PropertyOption.MANDATORY)) { prompt = INFO_EDITOR_PROMPT_READ_FIRST_VALUE.get(pd.getName()); } else { prompt = INFO_EDITOR_PROMPT_READ_FIRST_VALUE_OPTIONAL.get(pd.getName()); } app.println(); String s = app.readLineOfInput(prompt); if (s.trim().length() == 0 && !pd.hasOption(PropertyOption.MANDATORY)) { return; } T value = pd.decodeValue(s); if (values.contains(value)) { // Prevent addition of duplicates. app.println(); app.println(ERR_EDITOR_READ_FIRST_DUPLICATE.get(s)); } else { values.add(value); } break; } catch (PropertyException e) { app.errPrintln(); app.errPrintln(ArgumentExceptionFactory.adaptPropertyException(e, d).getMessageObject()); } } } if (pd.hasOption(PropertyOption.MULTI_VALUED)) { // Prompt for more values if multi-valued. while (true) { try { LocalizableMessage prompt = INFO_EDITOR_PROMPT_READ_NEXT_VALUE.get(pd.getName()); app.println(); String s = app.readLineOfInput(prompt); if (s.trim().length() == 0) { return; } T value = pd.decodeValue(s); if (values.contains(value)) { // Prevent addition of duplicates. app.println(); app.println(ERR_EDITOR_READ_NEXT_DUPLICATE.get(s)); } else { values.add(value); } } catch (PropertyException e) { app.errPrintln(); app.errPrintln(ArgumentExceptionFactory.adaptPropertyException(e, d).getMessageObject()); app.errPrintln(); } } } } /** * The threshold above which choice menus should be displayed in multiple columns. */ private static final int MULTI_COLUMN_THRESHOLD = 8; /** The application console. */ private final ConsoleApplication app; /** The management context. */ private final ManagementContext context; /** * The modifications performed: we assume that at most there is one modification per property definition. */ private final List<PropertyEditorModification<?>> mods = new ArrayList<>(); /** Whether the last type of choice made by the user in a menu is a reset. */ private boolean isLastChoiceReset; /** * Create a new property value editor which will read from the provided application console. * * @param app * The application console. * @param context * The management context. */ public PropertyValueEditor(ConsoleApplication app, ManagementContext context) { this.app = app; this.context = context; } /** * Interactively edits the properties of a managed object. Only the properties listed in the provided collection * will be accessible to the client. It is up to the caller to ensure that the list of properties does not include * read-only, monitoring, hidden, or advanced properties as appropriate. * * @param mo * The managed object. * @param c * The collection of properties which can be edited. * @param isCreate * Flag indicating whether or not the managed object is being created. If it is then read-only properties * will be modifiable. * @return Returns {@code MenuResult.success()} if the changes made to the managed object should be applied, or * {@code MenuResult.cancel()} if the user to chose to cancel any changes, or {@code MenuResult.quit()} if * the user chose to quit the application. * @throws ClientException * If the user input could not be retrieved for some reason. */ public MenuResult<Void> edit(ManagedObject<?> mo, Collection<PropertyDefinition<?>> c, boolean isCreate) throws ClientException { // Get values for this missing mandatory property. for (PropertyDefinition<?> pd : c) { if (pd.hasOption(PropertyOption.MANDATORY) && mo.getPropertyValues(pd).isEmpty()) { MandatoryPropertyInitializer mpi = new MandatoryPropertyInitializer(mo, pd); MenuResult<Void> result = mpi.invoke(app); if (!result.isSuccess()) { return result; } } } while (true) { // Construct the main menu. MenuBuilder<Boolean> builder = new MenuBuilder<>(app); String ufn = mo.getManagedObjectPath().getName(); if (ufn == null) { ufn = mo.getManagedObjectDefinition().getUserFriendlyName().toString(); } builder.setPrompt(INFO_EDITOR_HEADING_CONFIGURE_COMPONENT.get(ufn)); LocalizableMessage heading1 = INFO_DSCFG_HEADING_PROPERTY_NAME.get(); LocalizableMessage heading2 = INFO_DSCFG_HEADING_PROPERTY_VALUE.get(); builder.setColumnHeadings(heading1, heading2); builder.setColumnWidths(null, 0); // Create an option for editing/viewing each property. for (PropertyDefinition<?> pd : c) { // Determine whether this property should be modifiable. boolean isReadOnly = pd.hasOption(PropertyOption.MONITORING); if (!isCreate && pd.hasOption(PropertyOption.READ_ONLY)) { isReadOnly = true; } // Create the appropriate property action. MenuCallback<Boolean> callback; if (isReadOnly) { callback = new ReadOnlyPropertyViewer(mo, pd); } else if (pd.hasOption(PropertyOption.MULTI_VALUED)) { callback = new MultiValuedPropertyEditor(mo, pd); } else { callback = new SingleValuedPropertyEditor(mo, pd); } // Create the numeric option. LocalizableMessage values = getPropertyValues(pd, mo); builder.addNumberedOption(LocalizableMessage.raw("%s", pd.getName()), callback, values); } // Add a help option which displays a summary of the managed // object's definition. HelpCallback helpCallback = new ComponentHelpCallback(mo, c); builder.addHelpOption(helpCallback); // Add an option to apply the changes. if (isCreate) { builder.addCharOption(INFO_EDITOR_OPTION_FINISH_KEY.get(), INFO_EDITOR_OPTION_FINISH_CREATE_COMPONENT.get(ufn), MenuResult.success(true)); } else { builder.addCharOption(INFO_EDITOR_OPTION_FINISH_KEY.get(), INFO_EDITOR_OPTION_FINISH_MODIFY_COMPONENT.get(ufn), MenuResult.success(true)); } builder.setDefault(INFO_EDITOR_OPTION_FINISH_KEY.get(), MenuResult.success(true)); // Add options for canceling and quitting. if (app.isMenuDrivenMode()) { builder.addCancelOption(false); } builder.addQuitOption(); // Run the menu - success indicates that any changes should be // committed. app.println(); app.println(); Menu<Boolean> menu = builder.toMenu(); MenuResult<Boolean> result = menu.run(); if (result.isSuccess()) { if (result.getValue()) { return MenuResult.success(); } } else if (result.isCancel()) { return MenuResult.cancel(); } else { return MenuResult.quit(); } } } /** * Register the modification in the list of modifications. * * @param <T> * The type of the underlying property associated with the modification. * @param pd * the property definition. * @param newValues * the resulting values of the property once the modification is applied. * @param previousValues * the values we had before the modification is applied (these are not necessarily the *original* values * if we already have other modifications applied to the same property). */ private <T> void registerModification(PropertyDefinition<T> pd, SortedSet<T> newValues, SortedSet<T> previousValues) { if (isLastChoiceReset) { registerResetModification(pd, previousValues); } else if (!newValues.equals(previousValues)) { if (newValues.containsAll(previousValues)) { if (newValues.size() <= 1) { registerSetModification(pd, newValues, previousValues); } else { registerAddModification(pd, newValues, previousValues); } } else if (previousValues.containsAll(newValues)) { registerRemoveModification(pd, newValues, previousValues); } else if (newValues.size() <= 1) { registerSetModification(pd, newValues, previousValues); } else { // Split into two operations: remove and add SortedSet<T> removedValues = new TreeSet<>(previousValues); removedValues.removeAll(newValues); PropertyEditorModification<T> removeMod = PropertyEditorModification.createRemoveModification(pd, removedValues, previousValues); addModification(removeMod); SortedSet<T> retainedValues = new TreeSet<>(previousValues); retainedValues.retainAll(newValues); SortedSet<T> addedValues = new TreeSet<>(newValues); addedValues.removeAll(retainedValues); PropertyEditorModification<T> addMod = PropertyEditorModification.createAddModification(pd, addedValues, retainedValues); addModification(addMod); } } } /** * Register a reset modification in the list of modifications. * * @param <T> * The type of the underlying property associated with the modification. * @param pd * the property definition. * @param previousValues * the values we had before the modification is applied (these are not necessarily the *original* values * if we already have other modifications applied to the same property). */ private <T> void registerResetModification(PropertyDefinition<T> pd, SortedSet<T> previousValues) { PropertyEditorModification<?> mod = getModification(pd); SortedSet<T> originalValues; if (mod != null) { originalValues = new TreeSet<>(pd); castAndAddValues(originalValues, mod.getOriginalValues(), pd); removeModification(mod); } else { originalValues = new TreeSet<>(previousValues); } addModification(PropertyEditorModification.createResetModification(pd, originalValues)); } /** * Register a set modification in the list of modifications. * * @param <T> * The type of the underlying property associated with the modification. * @param pd * the property definition. * @param newValues * the resulting values of the property once the modification is applied. * @param previousValues * the values we had before the modification is applied (these are not necessarily the *original* values * if we already have other modifications applied to the same property). */ private <T> void registerSetModification(PropertyDefinition<T> pd, SortedSet<T> newValues, SortedSet<T> previousValues) { PropertyEditorModification<?> mod = getModification(pd); SortedSet<T> originalValues; if (mod != null) { originalValues = new TreeSet<>(pd); castAndAddValues(originalValues, mod.getOriginalValues(), pd); removeModification(mod); } else { originalValues = new TreeSet<>(previousValues); } addModification(PropertyEditorModification.createSetModification(pd, newValues, originalValues)); } /** * Register an add modification in the list of modifications. * * @param <T> * The type of the underlying property associated with the modification. * @param pd * the property definition. * @param newValues * the resulting values of the property once the modification is applied. * @param previousValues * the values we had before the modification is applied (these are not necessarily the *original* values * if we already have other modifications applied to the same property). */ private <T> void registerAddModification(PropertyDefinition<T> pd, SortedSet<T> newValues, SortedSet<T> previousValues) { PropertyEditorModification<?> mod = getModification(pd); PropertyEditorModification<T> newMod; SortedSet<T> originalValues; if (mod != null) { originalValues = new TreeSet<>(pd); castAndAddValues(originalValues, mod.getOriginalValues(), pd); if (mod.getType() == PropertyEditorModification.Type.ADD) { SortedSet<T> addedValues = new TreeSet<>(newValues); addedValues.removeAll(originalValues); newMod = PropertyEditorModification.createAddModification(pd, addedValues, originalValues); } else { newMod = PropertyEditorModification .createSetModification(pd, new TreeSet<T>(newValues), originalValues); } removeModification(mod); } else { originalValues = new TreeSet<>(previousValues); SortedSet<T> addedValues = new TreeSet<>(newValues); addedValues.removeAll(originalValues); newMod = PropertyEditorModification.createAddModification(pd, addedValues, originalValues); } addModification(newMod); } /** * Register a remove modification in the list of modifications. * * @param <T> * The type of the underlying property associated with the modification. * @param pd * the property definition. * @param newValues * the resulting values of the property once the modification is applied. * @param previousValues * the values we had before the modification is applied (these are not necessarily the *original* values * if we already have other modifications applied to the same property). */ private <T> void registerRemoveModification(PropertyDefinition<T> pd, SortedSet<T> newValues, SortedSet<T> previousValues) { PropertyEditorModification<?> mod = getModification(pd); PropertyEditorModification<T> newMod; SortedSet<T> originalValues; if (mod != null) { originalValues = new TreeSet<>(pd); castAndAddValues(originalValues, mod.getOriginalValues(), pd); if (newValues.isEmpty()) { newMod = PropertyEditorModification.createRemoveModification(pd, originalValues, originalValues); } else if (mod.getType() == PropertyEditorModification.Type.REMOVE) { SortedSet<T> removedValues = new TreeSet<>(originalValues); removedValues.removeAll(newValues); newMod = PropertyEditorModification.createRemoveModification(pd, removedValues, originalValues); } else { newMod = PropertyEditorModification .createSetModification(pd, new TreeSet<T>(newValues), originalValues); } removeModification(mod); } else { originalValues = new TreeSet<>(previousValues); SortedSet<T> removedValues = new TreeSet<>(originalValues); removedValues.removeAll(newValues); newMod = PropertyEditorModification.createRemoveModification(pd, removedValues, originalValues); } addModification(newMod); } /** * Returns the modifications that have been applied during the last call of the method PropertyValueEditor.edit. * * @return the modifications that have been applied during the last call of the method PropertyValueEditor.edit. */ public Collection<PropertyEditorModification<?>> getModifications() { return mods; } /** * Clears the list of modifications. */ public void resetModifications() { mods.clear(); } /** * Adds a modification to the list of modifications that have been performed. * * @param <T> * The type of the underlying property associated with the modification. * @param mod * the modification to be added. */ private <T> void addModification(PropertyEditorModification<T> mod) { mods.add(mod); } /** * Removes a modification from the list of modifications that have been performed. * * @param <T> * The type of the underlying property associated with the modification. * @param mod * the modification to be removed. */ private <T> boolean removeModification(PropertyEditorModification<T> mod) { return mods.remove(mod); } /** * Returns the modification associated with a given property definition: we assume that we have only one * modification per property definition (in the worst case we merge the modifications and generate a unique set * modification). * * @param <T> * The type of the underlying property associated with the modification. * @param pd * the property definition. * @return the modification associated with the provided property definition and <CODE>null</CODE> if no * modification could be found. */ private <T> PropertyEditorModification<?> getModification(PropertyDefinition<T> pd) { PropertyEditorModification<?> mod = null; for (PropertyEditorModification<?> m : mods) { if (pd.equals(m.getPropertyDefinition())) { mod = m; break; } } return mod; } /** * This method is required to avoid compilation warnings. It basically adds the contents of a collection to another * collection by explicitly casting its values. This is done because the method getModification() returns au * undefined type. * * @param <T> * The type of the destination values. * @param destination * the collection that we want to update. * @param source * the collection whose values we want to add (and cast) to the source collection. * @param pd * the PropertyDefinition we use to do the casting. * @throws ClassCastException * if an error occurs during the cast of the objects. */ private <T> void castAndAddValues(Collection<T> destination, Collection<?> source, PropertyDefinition<T> pd) { for (Object o : source) { destination.add(pd.castValue(o)); } } }