/** * */ package net.frontlinesms.ui.handler.contacts; import static net.frontlinesms.ui.UiGeneratorControllerConstants.COMPONENT_LABEL_STATUS; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import net.frontlinesms.AppProperties; import net.frontlinesms.data.DuplicateKeyException; import net.frontlinesms.data.domain.Contact; import net.frontlinesms.data.domain.Group; import net.frontlinesms.data.repository.ContactDao; import net.frontlinesms.data.repository.GroupMembershipDao; import net.frontlinesms.ui.Icon; import net.frontlinesms.ui.ThinletUiEventHandler; import net.frontlinesms.ui.UiGeneratorController; import net.frontlinesms.ui.handler.ChoiceDialogHandler; import net.frontlinesms.ui.i18n.CountryCallingCode; import net.frontlinesms.ui.i18n.InternationalisationUtils; /** * @author aga */ public class ContactEditor implements ThinletUiEventHandler, SingleGroupSelecterDialogOwner { //> STATIC CONSTANTS /** UI XML File Path: Edit and Create dialog for {@link Contact} objects */ private static final String UI_FILE_CREATE_CONTACT_FORM = "/ui/core/contacts/dgEditContact.xml"; private static final String COMPONENT_BT_REMOVE_FROM_GROUP = "btRemoveFromGroup"; private static final String COMPONENT_NEW_CONTACT_GROUP_LIST = "newContact_groupList"; private static final String COMPONENT_CONTACT_NAME = "contact_name"; private static final String COMPONENT_CONTACT_MOBILE_MSISDN = "contact_mobileMsisdn"; private static final String COMPONENT_CONTACT_OTHER_MSISDN = "contact_otherMsisdn"; private static final String COMPONENT_CONTACT_EMAIL_ADDRESS = "contact_emailAddress"; private static final String COMPONENT_CONTACT_NOTES = "contact_notes"; private static final String COMPONENT_CONTACT_DORMANT = "rb_dormant"; private static final String COMPONENT_RADIO_BUTTON_ACTIVE = "rb_active"; private static final String MESSAGE_EXISTENT_CONTACT = "message.contact.already.exists"; private static final String COMPONENT_SAVE_BUTTON = "btSave"; private static final String I18N_COMMON_USE = "common.use"; private static final String I18N_SENTENCE_TRY_INTERNATIONAL = "sentence.try.international"; //> INSTANCE PROPERTIES private Logger LOG = Logger.getLogger(this.getClass()); private UiGeneratorController ui; private ContactDao contactDao; private GroupMembershipDao groupMembershipDao; private ContactEditorOwner owner; /** UI Component: the dialog that will contain the contact editor */ private Object dialogComponent; /** UI Component: the list of groups that the contact is a member of */ private Object groupListComponent; /** The contact we are editing, or <code>null</code> if we are creating a new contact. */ private Contact target; /** Groups to add the contact to - this list contains only groups that he was not already a member of */ private Set<Group> addedGroups = new HashSet<Group>(); /** Groups to remove the contact from - this list contains only groups that he was already a member of */ private Set<Group> removedGroups = new HashSet<Group>(); /** A list of groups which should be disabled/hidden */ private List<Group> hiddenGroups; //> CONSTRUCTORS ContactEditor(UiGeneratorController ui, ContactEditorOwner owner) { this.ui = ui; this.contactDao = ui.getFrontlineController().getContactDao(); this.groupMembershipDao = ui.getFrontlineController().getGroupMembershipDao(); this.owner = owner; this.hiddenGroups = new LinkedList<Group>(); } //> INIT METHODS /** Show dialog to create a new contact in a particular group. */ public void show(Group selectedGroup) { initDialog(); if(selectedGroup != null && !selectedGroup.isRoot()) { addNewGroup(selectedGroup); } showDialog(); } /** Show dialog to edit an existing contact. */ public void show(Contact contact) { this.target = contact; this.hiddenGroups = this.groupMembershipDao.getGroups(target); initDialog(); populateContactDetails(contact); for(Group g : groupMembershipDao.getGroups(contact)) { addGroupToList(g); } showDialog(); } /** Adds {@link #dialogComponent} to {@link #ui} */ private void showDialog() { validateRequiredFields(); ui.add(this.dialogComponent); } /** Initialise the dialog from the UI layout file */ private void initDialog() { this.dialogComponent = ui.loadComponentFromFile(UI_FILE_CREATE_CONTACT_FORM, this); this.groupListComponent = find(COMPONENT_NEW_CONTACT_GROUP_LIST); } /** Populate the contact details ui components. */ private void populateContactDetails(Contact contact) { setText(COMPONENT_CONTACT_NAME, contact.getName()); setText(COMPONENT_CONTACT_MOBILE_MSISDN, contact.getPhoneNumber()); setText(COMPONENT_CONTACT_OTHER_MSISDN, contact.getOtherPhoneNumber()); setText(COMPONENT_CONTACT_EMAIL_ADDRESS, contact.getEmailAddress()); setText(COMPONENT_CONTACT_NOTES, contact.getNotes()); contactDetails_setActive(contact.isActive()); } //> GROUP SELECTION CALLBACKS /** @see SingleGroupSelecterDialogOwner#groupSelectionCompleted(Group) */ public void groupSelectionCompleted(Group group) { addNewGroup(group); } //> ACCESSORS /** @return new groups to add the contact to */ private Set<Group> getAddedGroups() { return addedGroups; } /** @return groups to remove the contact from */ private Set<Group> getRemovedGroups() { return removedGroups; } /** Adds the group to the UI, and if appropriate to {@link #addedGroups} */ private void addNewGroup(Group group) { hiddenGroups.add(group); if(!removedGroups.remove(group)) { addedGroups.add(group); } addGroupToList(group); } /** Removes group from the UI, and if appropriate from {@link #removedGroups} */ private void removeGroup(Group group) { if(!addedGroups.remove(group)) { removedGroups.add(group); } // Remove the Group from the list for(Object listItem : ui.getItems(this.groupListComponent)) { if(ui.getAttachedObject(listItem, Group.class).equals(group)) { ui.remove(listItem); break; } } } /** Adds a group to the list of groups */ private void addGroupToList(Group group) { ui.add(this.groupListComponent, createGroupListItem(group)); } /** @return thinlet list item for a group */ private Object createGroupListItem(Group group) { String path = group.getPath(); Object item = this.ui.createListItem(path, group); this.ui.setIcon(item, Icon.GROUP); return item; } //> UI EVENT METHODS /** Checks the contents of required fields, and if they aren't all set, disables the save button. */ public void validateRequiredFields() { boolean enableSaveButton = isRequiredFieldsFilled(); ui.setEnabled(find(COMPONENT_SAVE_BUTTON), enableSaveButton); } /** * Updates or create a contact with the details added by the user. <br> * This method is used by advanced mode, and also Contact Merge * TODO this method should be transactional * @param contactDetailsDialog */ public void save() { if(!isRequiredFieldsFilled()) { // Certain required details are missing. The save button should not be enabled // at this point, but this method may be called from UI components' "perform" // methods. return; } // Extract the new details of the contact from the UI String phoneNumber = getText(COMPONENT_CONTACT_MOBILE_MSISDN); if (!CountryCallingCode.isInInternationalFormat(phoneNumber)) { String internationalFormat = InternationalisationUtils.getInternationalPhoneNumber(phoneNumber); ChoiceDialogHandler choiceDialogHandler = new ChoiceDialogHandler(this.ui, this); choiceDialogHandler.setFirstButtonText(InternationalisationUtils.getI18nString(I18N_COMMON_USE, internationalFormat)); choiceDialogHandler.setFirstButtonIcon(ui.getFlagIconPath(AppProperties.getInstance().getUserCountry().toLowerCase())); choiceDialogHandler.setSecondButtonText(InternationalisationUtils.getI18nString(I18N_COMMON_USE, phoneNumber)); choiceDialogHandler.setSecondButtonIcon(Icon.TICK); choiceDialogHandler.showChoiceDialog(true, "doSave('true', choiceDialog)", "doSave('false', choiceDialog)", I18N_SENTENCE_TRY_INTERNATIONAL, internationalFormat); } else { this.doSave(false, null); } } public void doSave(String internationalisePhoneNumber, Object confirmDialog) { doSave(Boolean.valueOf(internationalisePhoneNumber), confirmDialog); } private void doSave(boolean internationalisePhoneNumber, Object confirmDialog) { if (confirmDialog != null) { this.removeDialog(confirmDialog); } String phoneNumber = getText(COMPONENT_CONTACT_MOBILE_MSISDN); if(internationalisePhoneNumber) { phoneNumber = InternationalisationUtils.getInternationalPhoneNumber(phoneNumber); } String name = getText(COMPONENT_CONTACT_NAME); String otherMsisdn = getText(COMPONENT_CONTACT_OTHER_MSISDN); String emailAddress = getText(COMPONENT_CONTACT_EMAIL_ADDRESS); String notes = getText(COMPONENT_CONTACT_NOTES); boolean isActive = contactDetails_getActive(); // Update or save the contact Contact contact = this.target; try { if (contact == null) { LOG.debug("Creating a new contact [" + name + ", " + phoneNumber + "]"); contact = new Contact(name, phoneNumber, otherMsisdn, emailAddress, notes, isActive); this.contactDao.saveContact(contact); // Update the groups that this contact is a member of for(Group g : getAddedGroups()) { groupMembershipDao.addMember(g, contact); } removeDialog(); owner.contactCreationComplete(contact); } else { // If this is not a new contact, we still need to update all details // that would otherwise be set by the constructor called in the block // above. LOG.debug("Editing contact [" + contact.getName() + "]. Setting new values!"); contact.setPhoneNumber(phoneNumber); contact.setName(name); contact.setOtherPhoneNumber(otherMsisdn); contact.setEmailAddress(emailAddress); contact.setNotes(notes); contact.setActive(isActive); // Update the groups that this contact is a member of for(Group g : getRemovedGroups()) { groupMembershipDao.removeMember(g, contact); } for(Group g : getAddedGroups()) { groupMembershipDao.addMember(g, contact); } this.contactDao.updateContact(contact); removeDialog(); owner.contactEditingComplete(contact); } } catch(DuplicateKeyException ex) { LOG.debug("There is already a contact with this mobile number - cannot save!", ex); showMergeContactDialog(contact, this.dialogComponent); } } /** Remove selected groups */ public void removeSelectedGroup() { Group selectedGroup = ui.getAttachedObject(ui.getSelectedItem(this.groupListComponent), Group.class); this.removeGroup(selectedGroup); this.hiddenGroups.remove(selectedGroup); this.ui.setEnabled(this.ui.find(COMPONENT_BT_REMOVE_FROM_GROUP), false); } /** * Called when a group is selected or deselected in the list */ public void groupSelectionChanged () { Object selected = ui.getSelectedItem(this.groupListComponent); this.ui.setEnabled(this.ui.find(COMPONENT_BT_REMOVE_FROM_GROUP), (selected != null)); } /** Show selecter for new groups. */ public void addNewGroup() { GroupSelecterDialog dialog = new GroupSelecterDialog(ui, this); dialog.init(ui.getRootGroup(), hiddenGroups); dialog.show(); } /** * Update the icon for active/dormant. * @param radioButton * @param label */ public void updateIconActive(Object radioButton, Object label) { String icon; if (this.ui.getName(radioButton).equals(COMPONENT_RADIO_BUTTON_ACTIVE)) { icon = Icon.ACTIVE; } else { icon = Icon.DORMANT; } this.ui.setIcon(label, icon); } /** Remove the dialog from view. */ public void removeDialog() { this.removeDialog(this.dialogComponent); } /** Remove a dialog from view. */ public void removeDialog(Object dialog) { this.ui.removeDialog(dialog); } //> UI HELPER METHODS /** Find a component in the contact edit dialog. */ private Object find(String componentName) { return this.ui.find(this.dialogComponent, componentName); } /** Check if all required fields have been filled */ private boolean isRequiredFieldsFilled() { return ui.getText(find(COMPONENT_CONTACT_NAME)).trim().length() > 0 && ui.getText(find(COMPONENT_CONTACT_MOBILE_MSISDN)).trim().length() > 0; } /** * Finds a child component by name, and then sets its text value. * @param parentComponent The parent component to search within * @param componentName The name of the child component * @param value the value to set the child's TEXT attribute to */ private void setText(String componentName, String value) { if(value == null) value = ""; this.ui.setText(find(componentName), value); } /** * Set the current state of the active/dormant component. * @param contactDetails * @param active */ private void contactDetails_setActive(boolean active) { this.ui.setSelected(find(COMPONENT_RADIO_BUTTON_ACTIVE), active); this.ui.setSelected(find(COMPONENT_CONTACT_DORMANT), !active); if (active) { this.ui.setIcon(find(COMPONENT_LABEL_STATUS), Icon.ACTIVE); } else { this.ui.setIcon(find(COMPONENT_LABEL_STATUS), Icon.DORMANT); } } /** * Finds a child component by name, and gets its text value. * @param parentComponent The parent component to search within * @param componentName The name of the child component * @return the text attribute of the child */ private String getText(String componentName) { return this.ui.getText(find(componentName)); } /** * @param contactDetails contact details dialog * @return the current state of the active component */ private boolean contactDetails_getActive() { return this.ui.isSelected(find(COMPONENT_RADIO_BUTTON_ACTIVE)); } /** * Show the form to allow merging between a previously-created contact, and an attempted-newly-created contact. * TODO if we work out a good-looking way of doing this, we should implement it. Currently this just warns the user that a contact with this number already exists. */ private void showMergeContactDialog(Contact oldContact, Object createContactForm) { // FIXME remove arguments from this method this.ui.alert(InternationalisationUtils.getI18nString(MESSAGE_EXISTENT_CONTACT)); } }