package net.frontlinesms.ui.handler.settings; import java.lang.reflect.*; import java.util.*; import net.frontlinesms.*; import net.frontlinesms.data.*; import net.frontlinesms.data.domain.*; import net.frontlinesms.events.EventBus; import net.frontlinesms.messaging.Provider; import net.frontlinesms.messaging.sms.events.InternetServiceEventNotification; import net.frontlinesms.messaging.sms.internet.SmsInternetService; import net.frontlinesms.messaging.sms.internet.SmsInternetServiceLoader; import net.frontlinesms.messaging.sms.properties.OptionalRadioSection; import net.frontlinesms.messaging.sms.properties.OptionalSection; import net.frontlinesms.messaging.sms.properties.PasswordString; import net.frontlinesms.messaging.sms.properties.PhoneSection; import net.frontlinesms.resources.UserHomeFilePropertySet; import net.frontlinesms.ui.ThinletUiEventHandler; import net.frontlinesms.ui.UiGeneratorController; import net.frontlinesms.ui.handler.contacts.ContactSelecter; import net.frontlinesms.ui.i18n.InternationalisationUtils; import org.apache.log4j.Logger; import thinlet.Thinlet; /** * Ui Handler for {@link SmsInternetServiceSettingsHandler} settings. * * @author Alex Anderson, Carlos Eduardo Genz */ public class SmsInternetServiceSettingsHandler implements ThinletUiEventHandler { //> CONSTANTS /** Path to XML for UI layout for settings screen, {@link #settingsDialog} */ private static final String UI_SETTINGS = "/ui/core/settings/services/internet/settings.xml"; /** Path to XML for UI layout for provider choosing screen, {@link #newServiceWizard} */ private static final String UI_CHOOSE_PROVIDER = "/ui/core/settings/services/internet/chooseProvider.xml"; /** Path to XML for UI layout for configuration screen, {@link #configurator} */ private static final String UI_CONFIGURE = "/ui/core/settings/services/internet/configure.xml"; private static final String UI_COMPONENT_LS_ACCOUNTS = "lsSmsInternetServices"; private static final String UI_COMPONENT_PN_BUTTONS = "pnButtons"; /** Logging object */ private static final Logger LOG = FrontlineUtils.getLogger(SmsInternetServiceSettingsHandler.class); //> INSTANCE PROPERTIES /** Thinlet instance that owns this handler */ private final UiGeneratorController controller; /** dialog for editing {@link SmsInternetService} settings, {@link SmsInternetServiceSettings} instances */ private Object settingsDialog; /** dialog for choosing the class of a new {@link SmsInternetService} */ private Object newServiceWizard; /** dialog for configuring a new {@link SmsInternetService} */ private Object configurator; /** Properties file containing mappings from proeprty names to the icons that should be displayed next to input fields for these properties. */ private IconMap iconProperties; /** All possible {@link SmsInternetService} classes available. */ private final Collection<Class<? extends SmsInternetService>> internetServiceProviders; private EventBus eventBus; //> CONSTRUCTORS /** * Creates a new instance of this UI. * @param controller thinlet controller that owns this {@link SmsInternetServiceSettingsHandler}. */ public SmsInternetServiceSettingsHandler(UiGeneratorController controller) { this.controller = controller; this.eventBus = controller.getFrontlineController().getEventBus(); iconProperties = new IconMap(FrontlineSMSConstants.PROPERTIES_SMS_INTERNET_ICONS); this.internetServiceProviders = new SmsInternetServiceLoader().getAll(); } /** Clears the desktop of all dialogs that this controls. */ private void clearDesktop() { if(newServiceWizard != null) removeDialog(newServiceWizard); } /** * Shows the general confirmation dialog (for removal). * @param methodToBeCalled the method to be called if the confirmation is affirmative */ public void showConfirmationDialog(String methodToBeCalled){ controller.showConfirmationDialog(methodToBeCalled, this); } /** Show this dialog to the user. */ public void showSettingsDialog() { clearDesktop(); settingsDialog = controller.loadComponentFromFile(UI_SETTINGS, this); // Update the list of accounts from the list provided Object accountList = controller.find(settingsDialog, UI_COMPONENT_LS_ACCOUNTS); this.refreshAccounts(accountList); selectionChanged(accountList, controller.find(settingsDialog, UI_COMPONENT_PN_BUTTONS)); controller.add(settingsDialog); } private void refreshAccounts(Object accountList) { if (accountList != null) { this.controller.removeAll(accountList); Collection<SmsInternetService> smsInternetServices = controller.getSmsInternetServices(); for (SmsInternetService service : smsInternetServices) { controller.add(accountList, controller.createListItem(getProviderName(service.getClass()) + " - " + service.getIdentifier(), service)); } } } /** Show the wizard for creating a new service. */ public void showNewServiceWizard() { clearDesktop(); newServiceWizard = controller.loadComponentFromFile(UI_CHOOSE_PROVIDER, this); Object providerList = controller.find(newServiceWizard, "lsProviders"); if (providerList != null) { for (Class<? extends SmsInternetService> provider : internetServiceProviders) { Object item = controller.createListItem(getProviderName(provider), provider.getCanonicalName()); String icon = getProviderIcon(provider); if (icon != null) { controller.setIcon(item, controller.getIcon(icon)); } controller.add(providerList, item); } } selectionChanged(providerList, controller.find(newServiceWizard, "pnButtons")); controller.add(newServiceWizard); } /** * Configure a provider given its UI component. * @param lsProviders */ public void configureService(Object lsProviders) { Object serviceComponent = this.controller.getSelectedItem(lsProviders); showConfigureService((SmsInternetService)controller.getAttachedObject(serviceComponent), settingsDialog); } /** * Removes the provided component from the view. * @param component */ public void removeDialog(Object component) { controller.remove(component); } /** * Configure a new provider. The class of this provider is provided as an attachment * to the selected list item in the provided list. * @param lsProviders */ @SuppressWarnings("unchecked") public void configureNewService(Object lsProviders) { Object selectedItem = controller.getSelectedItem(lsProviders); clearDesktop(); try { String providerClassName = (String)controller.getAttachedObject(selectedItem); LOG.info("Attempting to init SmsInternetService class: " + providerClassName); Class<? extends SmsInternetService> providerClass = (Class<? extends SmsInternetService>) Class.forName(providerClassName); SmsInternetService service = providerClass.getConstructor().newInstance(); showConfigureService(service, newServiceWizard); } catch(Throwable t) { LOG.warn("Error initialising SmsInternetService.", t); throw new RuntimeException(t); } } public void cancelAction(Object btCancel, Object dialog) { Object attached = controller.getAttachedObject(btCancel); removeDialog(dialog); if (attached != null) { controller.add(attached); } } /** * Enables/Disables fields from panel, according to list selection. * @param list * @param panel */ public void selectionChanged(Object list, Object panel) { for (Object item : controller.getItems(panel)) { String name = controller.getName(item); if (!"btNew".equals(name) && !"btCancel".equals(name)) { controller.setEnabled(item, controller.getSelectedItem(list) != null); } } } /** * Show the dialog for configuring a provider. * @param service */ public void showConfigureService(SmsInternetService service, Object fromDialog) { configurator = controller.loadComponentFromFile(UI_CONFIGURE, this); String icon = getProviderIcon(service.getClass()); if (icon != null) { controller.setIcon(configurator, controller.getIcon(icon)); } controller.setAttachedObject(configurator, service); controller.setText(configurator, getProviderName(service.getClass()) + " " + controller.getText(configurator)); Object configPanel = controller.find(configurator, "pnConfigFields"); Map<String, Object> properties = service.getPropertiesStructure(); if (service.getSettings() != null) { loadPropertiesFromDbIntoStructure(properties, service.getSettings().getProperties()); } for (String key : properties.keySet()) { Object value = properties.get(key); for (Object comp : getPropertyComponents(key, value)) controller.add(configPanel, comp); } if (fromDialog != null) { controller.setAttachedObject(controller.find(configurator, "btCancel"), fromDialog); if (fromDialog.equals(newServiceWizard)) { controller.setAttachedObject(controller.find(configurator, "btSave"), settingsDialog); } else { controller.setAttachedObject(controller.find(configurator, "btSave"), fromDialog); } } clearDesktop(); controller.add(configurator); } /** * @param properties * @param dbProperties */ @SuppressWarnings("unchecked") private void loadPropertiesFromDbIntoStructure(Map<String, Object> properties, Map<String, SmsInternetServiceSettingValue> dbProperties) { Map<String, Object> toUpdate = new LinkedHashMap<String, Object>(); for (String key : properties.keySet()) { Object value = properties.get(key); if (properties.get(key) instanceof OptionalSection) { OptionalSection section = (OptionalSection) value; value = section.getValue(); if (dbProperties.containsKey(key)) { value = SmsInternetServiceSettings.fromValue(section, dbProperties.get(key)); } section.setValue((Boolean) value); loadPropertiesFromDbIntoStructure(section.getDependencies(), dbProperties); toUpdate.put(key, section); } else if (properties.get(key) instanceof OptionalRadioSection) { OptionalRadioSection section = (OptionalRadioSection) value; value = section.getValue(); if (dbProperties.containsKey(key)) { OptionalRadioSection tmp = (OptionalRadioSection) SmsInternetServiceSettings.fromValue(section, dbProperties.get(key)); section.setValue(tmp.getValue()); value = section.getValue(); } Enum enumm = (Enum) value; section.setValue(enumm); try { Method getValues = enumm.getClass().getMethod("values"); Enum[] vals = (Enum[]) getValues.invoke(null); for (Enum val : vals) { loadPropertiesFromDbIntoStructure(section.getDependencies(val), dbProperties); } } catch (Throwable t) { LOG.error("Could not get values from enum.", t); } toUpdate.put(key, section); } else { if (dbProperties.containsKey(key)) { value = SmsInternetServiceSettings.fromValue(value, dbProperties.get(key)); toUpdate.put(key, value); } } } for (String key : toUpdate.keySet()) { properties.put(key, toUpdate.get(key)); } } /** Confirms deletes of {@link SmsInternetService}(s) from the system and removes them from the list of services */ public void removeServices() { controller.removeConfirmationDialog(); removeServices(controller.find(settingsDialog, "lsSmsInternetServices")); } /** * Delete the selected services from the system and remove them from the list. * @param lsProviders */ private void removeServices(Object lsProviders) { Object[] obj = controller.getSelectedItems(lsProviders); for (Object object : obj) { SmsInternetService service = (SmsInternetService) controller.getAttachedObject(object); controller.getSmsInternetServiceSettingsDao().deleteSmsInternetServiceSettings(service.getSettings()); controller.remove(object); this.eventBus.notifyObservers(new InternetServiceEventNotification(InternetServiceEventNotification.EventType.DELETE, service)); } selectionChanged(lsProviders, controller.find(settingsDialog, "pnButtons")); } /** * Gets a Thinlet UI component for configuring this property. The current value of the property will * be inserted into the UI component. * @param key key for the property * @param valueObj current value of the property * @return UI components for the property */ @SuppressWarnings("unchecked") private Object[] getPropertyComponents(String key, Object valueObj) { Object[] components; String label; try { label = InternationalisationUtils.getI18nString(key); } catch(MissingResourceException ex) { label = key; } String valueString = SmsInternetServiceSettings.toValue(valueObj).getValue(); if(valueObj instanceof String || valueObj instanceof Integer || valueObj instanceof PasswordString) { // FIXME can we clean up this use of valueString here? Surely password string should have // only one extra thing: thinlet.setBoolean(tf, "hidden", true) ? // If we have a db value, use that cos it's the right one components = new Object[2]; components[0] = controller.createLabel(label); if (iconProperties.hasIcon(key)) { controller.setIcon(components[0], controller.getIcon(iconProperties.getIcon(key))); } Object tf; if (valueObj instanceof PasswordString) { tf = controller.createPasswordfield(key, ((PasswordString)valueObj).getValue()); } else { tf = controller.createTextfield(key, valueString); } controller.setColumns(tf, 25); controller.setInteger(tf, "weightx", 1); components[1] = tf; } else if(valueObj instanceof Boolean) { //If we have a db value, use that cos it's the right one Object checkbox = controller.createCheckbox(key, label, Boolean.parseBoolean(valueString)); if (iconProperties.hasIcon(key)) { controller.setIcon(checkbox, controller.getIcon(iconProperties.getIcon(key))); } controller.setColspan(checkbox, 2); components = new Object[] {checkbox}; } else if (valueObj instanceof PhoneSection) { Object panel = controller.createPanel("pn" + key.replace(".", "_")); controller.setInteger(panel, "gap", 5); controller.setInteger(panel, "weightx", 1); Object lb = controller.createLabel(label); if (iconProperties.hasIcon(key)) { controller.setIcon(lb, controller.getIcon(iconProperties.getIcon(key))); } //If we have a db value, use that cos it's the right one Object tf = controller.createTextfield(key, valueString); controller.setInteger(tf, "weightx", 1); //controller.setInteger(tf, Thinlet.ATTRIBUTE_COLUMNS, 20); Object bt = controller.createButton(""); controller.setIcon(bt, controller.getIcon(PhoneSection.BUTTON_ICON)); controller.setAttachedObject(bt, tf); controller.add(panel, tf); controller.add(panel, bt); controller.setAction(bt, "showContacts(this)", panel, this); components = new Object[] {lb, panel}; } else if (valueObj instanceof OptionalSection) { OptionalSection section = (OptionalSection) valueObj; boolean toSet = Boolean.parseBoolean(valueString); Object checkbox = controller.createCheckbox(key, label, toSet); if (iconProperties.hasIcon(key)) { controller.setIcon(checkbox, controller.getIcon(iconProperties.getIcon(key))); } controller.setColspan(checkbox, 2); Object panel = controller.createPanel("pn" + key.replace(".", "_")); controller.setColspan(panel, 2); controller.setColumns(panel, 2); controller.setGap(panel, 8); controller.setInteger(panel, "top", 10); // TODO these should call methods in ExtendedThinlet controller.setInteger(panel, "right", 10); controller.setInteger(panel, "left", 10); controller.setInteger(panel, "bottom", 10); controller.setBorder(panel, true); List<Object> objects = new LinkedList<Object>(); objects.add(checkbox); for (String child : section.getDependencies().keySet()) { for (Object comp : getPropertyComponents(child, section.getDependencies().get(child))) { controller.add(panel, comp); } } objects.add(panel); components = objects.toArray(); controller.setAction(checkbox, "enableFields(this.selected, " + controller.getName(panel) + ")", panel, this); enableFields(controller.isSelected(checkbox), panel); } else if (valueObj instanceof OptionalRadioSection) { OptionalRadioSection section = (OptionalRadioSection) valueObj; Object panel = controller.createPanel(key); controller.setColspan(panel, 2); controller.setColumns(panel, 1); controller.setGap(panel, 8); controller.setInteger(panel, "top", 10); // TODO these should call methods in ExtendedThinlet controller.setInteger(panel, "right", 10); controller.setInteger(panel, "left", 10); controller.setInteger(panel, "bottom", 10); controller.setInteger(panel, "weightx", 1); controller.setBorder(panel, true); controller.setText(panel, label); if (iconProperties.hasIcon(key)) { controller.setIcon(panel, controller.getIcon(iconProperties.getIcon(key))); } valueString = valueString.substring(valueString.lastIndexOf(".") + 1); try { Method getValues = section.getValue().getClass().getMethod("values"); Enum[] vals = (Enum[]) getValues.invoke(null); for (Enum val : vals) { Object rb = controller.createRadioButton(key + val.name(), val.name(), key, val.name().equals(valueString)); controller.add(panel, rb); Map<String, Object> child = section.getDependencies(val); Object panelChild = controller.createPanel(key + val.ordinal()); controller.setColspan(panelChild, 2); controller.setColumns(panelChild, 2); controller.setGap(panelChild, 8); controller.setInteger(panelChild, "top", 10); controller.setInteger(panelChild, "right", 10); controller.setInteger(panelChild, "left", 10); controller.setInteger(panelChild, "bottom", 10); controller.setInteger(panelChild, "weightx", 1); for (String childKey : child.keySet()) { for (Object comp : getPropertyComponents(childKey, child.get(childKey))) { controller.add(panelChild, comp); } } controller.add(panel, panelChild); controller.setAttachedObject(rb, panelChild); controller.setAction(rb, "enableFields(" + controller.getName(panel) + ")", panel, this); } enableFields(panel); } catch (Throwable t) { LOG.error("Could not get values from enum [" + valueObj.getClass() + "]", t); } components = new Object[] {panel}; } else if (valueObj instanceof Enum<?>) { components = new Object[1]; Object panel = controller.createPanel(key); controller.setColspan(panel, 2); controller.setColumns(panel, 1); controller.setInteger(panel, "gap", 8); controller.setInteger(panel, "top", 10); controller.setInteger(panel, "right", 10); controller.setInteger(panel, "left", 10); controller.setInteger(panel, "bottom", 10); controller.setBorder(panel, true); controller.setText(panel, label); if (iconProperties.hasIcon(key)) { controller.setIcon(panel, controller.getIcon(iconProperties.getIcon(key))); } try { Method getValues = valueObj.getClass().getMethod("values"); Enum[] vals = (Enum[]) getValues.invoke(null); for (Enum val : vals) { controller.add(panel, controller.createRadioButton(key + val.name(), val.name(), key, val.name().equals(valueString))); } } catch (Throwable t) { LOG.error("Could not get values from enum [" + valueObj.getClass() + "]", t); } components[0] = panel; } else throw new RuntimeException("Unsupported property type for property '"+key+"': " + valueObj.getClass()); return components; } public void showContacts(Object button) { Object textField = controller.getAttachedObject(button); ContactSelecter contactSelecter = new ContactSelecter(controller); final boolean shouldHaveEmail = false; contactSelecter.show(InternationalisationUtils.getI18nString(FrontlineSMSConstants.COMMON_SENDER_NUMBER), "setContactNumber(contactSelecter_contactList, contactSelecter)", textField, this, shouldHaveEmail); } public void setContactNumber(Object list, Object dialog) { Object textField = controller.getAttachedObject(dialog); Object selectedItem = controller.getSelectedItem(list); if (selectedItem == null) { controller.alert(InternationalisationUtils.getI18nString(FrontlineSMSConstants.MESSAGE_NO_CONTACT_SELECTED)); return; } Contact selectedContact = controller.getContact(selectedItem); controller.setText(textField, selectedContact.getPhoneNumber()); removeDialog(dialog); } public void enableFields(boolean checked, Object panel) { controller.setEnabled(panel, checked); for (Object obj : controller.getItems(panel)) { enableFields(checked, obj); } } public void enableFields(Object panel) { for (Object child : controller.getItems(panel)) { if (Thinlet.getClass(child).equals(Thinlet.WIDGET_CHECKBOX)) { Object childPanel = controller.getAttachedObject(child); enableFields(controller.isSelected(child), childPanel); } } } /** * Gets the value of a property from its UI components. This method reverses the action of * {@link #getPropertyComponents(String, Object)} * @param comp the ui component containing this property value * @param clazz The class of this property value * @return */ private Object getPropertyValue(Object comp, Class<?> clazz) { if(clazz.equals(String.class)) return controller.getText(comp); if(clazz.equals(Integer.class)) return Integer.parseInt(controller.getText(comp)); if(clazz.equals(Boolean.class)) return new Boolean(controller.isSelected(comp)); if(clazz.equals(PasswordString.class)) return new PasswordString(controller.getText(comp)); if (clazz.equals(OptionalSection.class)) return new Boolean(controller.isSelected(comp)); if(clazz.equals(PhoneSection.class)) return new PhoneSection(controller.getText(comp)); if (clazz.equals(OptionalRadioSection.class)) { for (Object child : controller.getItems(comp)) { if (Thinlet.getClass(child).equals(Thinlet.WIDGET_CHECKBOX) && controller.isSelected(child)) { return controller.getText(child); } } } if (clazz.isEnum()) { for (Object child : controller.getItems(comp)) { if (Thinlet.getClass(child).equals(Thinlet.WIDGET_CHECKBOX) && controller.isSelected(child)) { return controller.getText(child); } } } throw new RuntimeException("Unsupported property type: " + clazz); } /** * Save the settings of the {@link SmsInternetService} and return to the main settings dialog. * @param pnSmsInternetServiceConfigure * @throws DuplicateKeyException */ public void saveSettings(Object pnSmsInternetServiceConfigure, Object btSave) throws DuplicateKeyException { SmsInternetService service = (SmsInternetService)controller.getAttachedObject(pnSmsInternetServiceConfigure); SmsInternetServiceSettings serviceSettings = service.getSettings(); if (serviceSettings == null) { serviceSettings = new SmsInternetServiceSettings(service); controller.getSmsInternetServiceSettingsDao().saveSmsInternetServiceSettings(serviceSettings); } Map<String, Object> properties = service.getPropertiesStructure(); saveSettings(pnSmsInternetServiceConfigure, serviceSettings, properties); service.setSettings(serviceSettings); controller.getSmsInternetServiceSettingsDao().updateSmsInternetServiceSettings(service.getSettings()); // Add this service to the frontline controller. TODO surely there is a nicer way of doing this? removeDialog(pnSmsInternetServiceConfigure); this.eventBus.notifyObservers(new InternetServiceEventNotification(InternetServiceEventNotification.EventType.ADD, service)); } @SuppressWarnings("unchecked") private void saveSettings(Object pnSmsInternetServiceConfigure, SmsInternetServiceSettings serviceSettings, Map<String, Object> properties) { for(String key : properties.keySet()) { Object propertyUiComponent = controller.find(pnSmsInternetServiceConfigure, key); Object newValue = getPropertyValue(propertyUiComponent, properties.get(key).getClass()); if (properties.get(key) instanceof OptionalSection) { OptionalSection section = (OptionalSection) properties.get(key); section.setValue((Boolean) newValue); serviceSettings.set(key, section); saveSettings(pnSmsInternetServiceConfigure, serviceSettings, section.getDependencies()); } else if (properties.get(key) instanceof OptionalRadioSection) { OptionalRadioSection section = (OptionalRadioSection) properties.get(key); try { Method getValueOf = section.getValue().getClass().getMethod("valueOf", String.class); Enum enumm = (Enum) getValueOf.invoke(null, newValue); section.setValue(enumm); serviceSettings.set(key, section); Method getValues = enumm.getClass().getMethod("values"); Enum[] vals = (Enum[]) getValues.invoke(null); for (Enum val : vals) { saveSettings(pnSmsInternetServiceConfigure, serviceSettings, section.getDependencies(val)); } } catch (Throwable t) { LOG.error("Could not get values from enum.", t); } } else { serviceSettings.set(key, newValue); } } } /** * Gets the icon associated with a particular {@link SmsInternetService}. * @param clazz The class of the {@link SmsInternetService}. * @return the path at which the icon file is located */ public static String getProviderIcon(Class<?> clazz) { String ret = null; //Default return value if (clazz.isAnnotationPresent(Provider.class)) { Provider provider = clazz.getAnnotation(Provider.class); if (provider != null && !provider.icon().equals("")) { ret = provider.icon(); } } return ret; } /** * Gets the name associated with a particular {@link SmsInternetService}. * @param clazz The class of the {@link SmsInternetService}. * @return the name to display for a provider */ public static String getProviderName(Class<?> clazz) { String ret = clazz.getCanonicalName(); //Default return value if (clazz.isAnnotationPresent(Provider.class)) { Provider provider = clazz.getAnnotation(Provider.class); if (provider != null && !provider.name().equals("")) { ret = provider.name(); } } return ret; } } final class IconMap extends UserHomeFilePropertySet { //> STATIC CONSTANTS //> INSTANCE PROPERTIES //> CONSTRUCTORS /** * Set up a new {@link IconMap}. * @param name the name of the icon map */ IconMap(String name) { super(name); } //> ACCESSORS /** * Check if there is an icon available. * @param key The key for the icon. * @return <code>true</code> if there is an icon specified for this key; <code>false</code> otherwise. */ public boolean hasIcon(String key) { return this.getIcon(key) != null; } /** * Get the icon path for the specified key. * @param key * @return */ public String getIcon(String key) { return super.getProperty(key); } //> INSTANCE HELPER METHODS //> STATIC FACTORIES //> STATIC HELPER METHODS }