From cc760bfd15caadb56bfef477cb54dc94c25f7fa7 Mon Sep 17 00:00:00 2001 From: Mili Verma Date: Fri, 6 Jan 2012 15:00:55 +0000 Subject: Port Adhoc commands to Stroke This patch ports the Adhoc commands from Swiften to Stroke. It also ports their unit tests. Test-information: Unit tests pass. MLC able to use the ad-hoc command fine. diff --git a/src/com/isode/stroke/adhoc/OutgoingAdHocCommandSession.java b/src/com/isode/stroke/adhoc/OutgoingAdHocCommandSession.java new file mode 100644 index 0000000..a9db0b6 --- /dev/null +++ b/src/com/isode/stroke/adhoc/OutgoingAdHocCommandSession.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010-2011 Kevin Smith + * All rights reserved. + */ + +package com.isode.stroke.adhoc; + +import java.util.HashMap; +import java.util.List; + +import com.isode.stroke.elements.Command; +import com.isode.stroke.elements.ErrorPayload; +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.IQ; +import com.isode.stroke.elements.Command.Action; +import com.isode.stroke.elements.Command.Status; +import com.isode.stroke.jid.JID; +import com.isode.stroke.queries.GenericRequest; +import com.isode.stroke.queries.IQRouter; +import com.isode.stroke.signals.Signal1; +import com.isode.stroke.signals.Slot2; + +/** + * This class maintains the session between the client and the server for an + * Ad-Hoc command. + */ +public class OutgoingAdHocCommandSession { + /** + * Availability of action. + */ + public enum ActionState { + /** + * Action isn't applicable to this command + */ + ABSENT, + /** + * Action is applicable to this command but not currently available + */ + PRESENT, + /** + * Action is currently available (not used in + * {@link OutgoingAdHocCommandSession}) + */ + ENABLED, + /** + * Action is applicable and currently available + */ + ENABLED_AND_PRESENT + }; + + /** + * Emitted when the form for the next stage is available. The client should + * add a listener to this signal which will be called when the server sends + * a form. + */ + public final Signal1 onNextStageReceived = new Signal1(); + + /** + * Emitted on error. The client should add a listener to this signal which + * will be called when the server sends an error. + */ + public final Signal1 onError = new Signal1(); + + private JID to_; + private String commandNode_; + private IQRouter iqRouter_; + private boolean isMultiStage_; + private String sessionID_; + private HashMap actionStates_ = new HashMap(); + + /** + * Create an Ad-Hoc command session. The initial command will be sent to the + * server on calling {@link #start()}. + * + * @param to JID of the user for which the Ad-Hoc command is executed, must + * not be null + * @param commandNode Node part of the Ad-Hoc command as published by the + * server (e.g. "http://isode.com/xmpp/commands#test"), must not + * be null + * @param iqRouter TODO: not sure how to explain this, must not be null + */ + public OutgoingAdHocCommandSession(JID to, String commandNode, + IQRouter iqRouter) { + if (to == null) { + throw new NullPointerException("'to' must not be null"); + } + if (commandNode == null) { + throw new NullPointerException("'commandNode' must not be null"); + } + if (iqRouter == null) { + throw new NullPointerException("'iqRouter' must not be null"); + } + + to_ = to; + commandNode_ = commandNode; + iqRouter_ = iqRouter; + isMultiStage_ = false; + } + + private void handleResponse(Command payload, ErrorPayload error) { + if (error != null) { + onError.emit(error); + } else { + List actions = payload.getAvailableActions(); + actionStates_.clear(); + if (payload.getStatus() == Status.EXECUTING) { + actionStates_.put(Action.CANCEL, + ActionState.ENABLED_AND_PRESENT); + actionStates_.put(Action.COMPLETE, ActionState.PRESENT); + if (actions.contains(Action.COMPLETE)) { + actionStates_.put(Action.COMPLETE, + ActionState.ENABLED_AND_PRESENT); + } + + if (getIsMultiStage()) { + actionStates_.put(Action.NEXT, ActionState.PRESENT); + actionStates_.put(Action.PREV, ActionState.PRESENT); + } + + if (actions.contains(Action.NEXT)) { + actionStates_.put(Action.NEXT, + ActionState.ENABLED_AND_PRESENT); + } + + if (actions.contains(Action.PREV)) { + actionStates_.put(Action.PREV, + ActionState.ENABLED_AND_PRESENT); + } + } + + sessionID_ = payload.getSessionID(); + if (actions.contains(Action.NEXT) || actions.contains(Action.PREV)) { + isMultiStage_ = true; + } + onNextStageReceived.emit(payload); + } + } + + /** + * @return true if the form is multi-stage. Will return a valid result only + * after the first response is received from the server so should be + * called only after the listener for {@link #onNextStageReceived} + * has been called at least once. + */ + public boolean getIsMultiStage() { + return isMultiStage_; + } + + /** + * Send initial request to the target. + */ + public void start() { + Action action = null; + GenericRequest commandRequest = new GenericRequest( + IQ.Type.Set, to_, new Command(commandNode_, null, action), + iqRouter_); + commandRequest.onResponse.connect(new Slot2() { + public void call(Command payload, ErrorPayload error) { + handleResponse(payload, error); + } + }); + commandRequest.send(); + } + + /** + * Cancel command session with the target. + */ + public void cancel() { + if (sessionID_.length() != 0) { + submitForm(null, Action.CANCEL); + } + } + + /** + * Return to the previous stage. + */ + public void goBack() { + submitForm(null, Action.PREV); + } + + /** + * Send the form to complete the command. + * + * @param form Form for submission - if null, the command will be submitted + * with no form. + */ + public void complete(Form form) { + submitForm(form, Action.COMPLETE); + } + + /** + * Send the form to advance to the next stage of the command. + * + * @param form Form for submission - if null, the command will be submitted + * with no form. + */ + public void goNext(Form form) { + submitForm(form, Action.NEXT); + } + + private void submitForm(Form form, Action action) { + Command command = new Command(commandNode_, sessionID_, action); + command.setForm(form); + + GenericRequest commandRequest = new GenericRequest( + IQ.Type.Set, to_, command, iqRouter_); + commandRequest.onResponse.connect(new Slot2() { + public void call(Command payload, ErrorPayload error) { + handleResponse(payload, error); + } + }); + commandRequest.send(); + } + + /** + * Get the state of a given action. This is useful for a UI to determine + * which buttons should be visible, and which enabled. If no actions are + * {@link ActionState#ENABLED_AND_PRESENT}, the command has completed. + * + *

+ * Will return a valid result for the current stage only after the response + * is received from the server so should be called only after the listener + * for {@link #onNextStageReceived} has been called for that stage. + * + * @param action Action for which the state needs to be determined for the + * current form. Useful for Next, Prev, Cancel and Complete only, + * for other values and null, {@link ActionState#ABSENT} will be + * returned. + * @return state of the requested action, will never be null + */ + public ActionState getActionState(Action action) { + ActionState actionState = actionStates_.get(action); + if (actionState == null) { + actionState = ActionState.ABSENT; + } + + return actionState; + } + + @Override + public String toString() { + return OutgoingAdHocCommandSession.class.getSimpleName() + "\nto: " + + to_ + "\ncommand node: " + commandNode_ + "\nsession ID: " + + sessionID_ + "\nis multi-stage: " + isMultiStage_; + } +} diff --git a/src/com/isode/stroke/elements/Command.java b/src/com/isode/stroke/elements/Command.java new file mode 100644 index 0000000..75f3bf2 --- /dev/null +++ b/src/com/isode/stroke/elements/Command.java @@ -0,0 +1,473 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Kevin Smith + * All rights reserved. + */ + +package com.isode.stroke.elements; + +import java.util.ArrayList; +import java.util.List; + +/** + * Ad-Hoc Command (XEP-0050). + */ +public class Command extends Payload { + /** + * Attribute "node" + */ + public static final String COMMAND_ATTRIBUTE_NODE = "node"; + + /** + * Attribute "sessionid" + */ + public static final String COMMAND_ATTRIBUTE_SESSION_ID = "sessionid"; + + /** + * Attribute "status" + */ + public static final String COMMAND_ATTRIBUTE_STATUS = "status"; + + /** + * Attribute "action" + */ + public static final String COMMAND_ATTRIBUTE_ACTION = "action"; + + /** + * Element "actions" + */ + public static final String COMMAND_ELEMENT_ACTIONS = "actions"; + + /** + * Attribute "execute" + */ + public static final String COMMAND_ATTRIBUTE_EXECUTE = "execute"; + + /** + * Element "note" + */ + public static final String COMMAND_ELEMENT_NOTE = "note"; + + /** + * Attribute "type" + */ + public static final String COMMAND_ATTRIBUTE_TYPE = "type"; + + /** + * Current status of the command. + */ + public enum Status { + /** + * The command is being executed. + */ + EXECUTING("executing"), + /** + * The command has completed. The command session has ended. + */ + COMPLETED("completed"), + /** + * The command has been cancelled. The command session has ended. + */ + CANCELED("canceled"), + /** + * No status has been set for the command. + */ + NO_STATUS(""); + + private String stringForm_; + + private Status(String stringForm) { + stringForm_ = stringForm; + } + + /** + * Get status from its string form. + * + * @param stringForm String form of status, can be null + * + * @return Corresponding status if match found, otherwise + * {@link #NO_STATUS}. Will never be null. + */ + public static Status getStatus(String stringForm) { + if (stringForm != null) { + for (Status status : Status.values()) { + if (status.stringForm_.equals(stringForm)) { + return status; + } + } + } + + return NO_STATUS; + } + + /** + * @return String form of status, will never be null + */ + public String getStringForm() { + return stringForm_; + } + }; + + /** + * The action to undertake with the given command. + */ + public enum Action { + /** + * The command should be cancelled. + */ + CANCEL("cancel"), + /** + * The command should be executed or continue to be executed. + */ + EXECUTE("execute"), + /** + * The command should be completed (if possible). + */ + COMPLETE("complete"), + /** + * The command should digress to the previous stage of execution. + */ + PREV("prev"), + /** + * The command should progress to the next stage of execution. + */ + NEXT("next"), + /** + * No action is available for the command. + */ + NO_ACTION(""); + + private String stringForm_; + + private Action(String stringForm) { + stringForm_ = stringForm; + } + + /** + * Get action from its string form. + * + * @param stringForm String form of action, can be null + * + * @return Corresponding action if match found, otherwise + * {@link Action#NO_ACTION}. Will never be null. + */ + public static Action getAction(String stringForm) { + if (stringForm != null) { + for (Action action : Action.values()) { + if (action.stringForm_.equals(stringForm)) { + return action; + } + } + } + + return NO_ACTION; + } + + /** + * @return String form of action, will never be null + */ + public String getStringForm() { + return stringForm_; + } + }; + + /** + * This class contains information about the current status of the command. + * TODO: I am not sure why this is not an immutable class. + */ + public static class Note { + /** + * Severity of the note. + */ + public enum Type { + /** + * The note is informational only. This is not really an exceptional + * condition. + */ + INFO("info"), + /** + * The note indicates a warning. Possibly due to illogical (yet + * valid) data. + */ + WARN("warn"), + /** + * The note indicates an error. The text should indicate the reason + * for the error. + */ + ERROR("error"); + + private String stringForm_; + + private Type(String stringForm) { + stringForm_ = stringForm; + } + + /** + * Get type from its string form. + * + * @param stringForm String form of type, can be null + * + * @return Corresponding type if match found, otherwise + * {@link Type#INFO}. Will never be null. + */ + public static Type getType(String stringForm) { + if (stringForm != null) { + for (Type type : Type.values()) { + if (type.stringForm_.equals(stringForm)) { + return type; + } + } + } + + return INFO; + } + + /** + * @return String form of type, will never be null + */ + public String getStringForm() { + return stringForm_; + } + }; + + /** + * Create a note element for the Ad-Hoc command. + * + * @param note user-readable text, can be null which will be stored as + * an empty string + * @param type Severity of the note, can be null which will be stored as + * {@link Type#INFO} + */ + public Note(String note, Type type) { + this.note = (note != null) ? note : ""; + this.type = (type != null) ? type : Type.INFO; + } + + /** + * User-readable text, will never be null + */ + public final String note; + + /** + * Severity of the note, will never be null + */ + public final Type type; + }; + + private String node_; + private String sessionID_; + private Action action_; + private Status status_; + private Action executeAction_; + private List availableActions_ = new ArrayList(); + private List notes_ = new ArrayList(); + private Form form_ = null; + + private void assignData(String node, String sessionID, Action action, + Status status) { + setNode(node); + setSessionID(sessionID); + setAction(action); + setStatus(status); + setExecuteAction(null); + } + + /** + * Create an Ad-Hoc command with the given node, session ID, status and + * {@link Action#NO_ACTION} action. + * + * @param node Command identification, can be null which will be stored as + * an empty string + * @param sessionID The ID of the session within which the command exists, + * can be null (which will be stored as an empty string) or empty + * if this is the first stage of the command and the client does + * not know it yet + * @param status Status of the command, can be null which will be stored as + * {@link Status#NO_STATUS} + */ + public Command(String node, String sessionID, Status status) { + assignData(node, sessionID, Action.NO_ACTION, status); + } + + /** + * Create an Ad-Hoc command with the given node, session ID, action and + * {@link Status#NO_STATUS} status. + * + * @param node Command identification, can be null which will be stored as + * an empty string + * @param sessionID The ID of the session within which the command exists, + * can be null (which will be stored as an empty string) or empty + * if this is the first stage of the command and the client does + * not know it yet + * @param action action of the command, can be null which will be stored as + * {@link Action#EXECUTE} + */ + public Command(String node, String sessionID, Action action) { + assignData(node, sessionID, action, null); + } + + /** + * Create an Ad-Hoc command with an empty node, empty session ID, + * {@link Action#EXECUTE} action and {@link Status#NO_STATUS} status. + */ + public Command() { + this(null, null, (Action) null); + } + + /** + * @return The command ID, will never be null + */ + public String getNode() { + return node_; + } + + /** + * Set command ID. + * + * @param node Command identification, can be null which will be stored as + * an empty string + */ + public void setNode(String node) { + node_ = (node != null) ? node : ""; + } + + /** + * @return The ID of the session within which the command exists, can be + * empty if this is the first stage of the command and the client + * does not know it yet, will never be null + */ + public String getSessionID() { + return sessionID_; + } + + /** + * Set session ID. + * + * @param sessionID The ID of the session within which the command exists, + * can be null (which will be stored as an empty string) or empty + * if this is the first stage of the command and the client does + * not know it yet + */ + public void setSessionID(String sessionID) { + sessionID_ = (sessionID != null) ? sessionID : ""; + } + + /** + * @return action of the command, will never be null + */ + public Action getAction() { + return action_; + } + + /** + * Set action of the command. + * + * @param action action of the command, can be null which will be stored as + * {@link Action#EXECUTE} + */ + public void setAction(Action action) { + action_ = (action != null) ? action : Action.EXECUTE; + } + + /** + * @return execute action of the command, will never be null + */ + public Action getExecuteAction() { + return executeAction_; + } + + /** + * Set execute action of the command. + * + * @param action execute action of the command, can be null which will be + * stored as {@link Action#NO_ACTION} + */ + public void setExecuteAction(Action action) { + executeAction_ = (action != null) ? action : Action.NO_ACTION; + } + + /** + * @return status of the command, will never be null + */ + public Status getStatus() { + return status_; + } + + /** + * Set status of the command. + * + * @param status Status of the command, can be null which will be stored as + * {@link Status#NO_STATUS} + */ + public void setStatus(Status status) { + status_ = (status != null) ? status : Status.NO_STATUS; + } + + /** + * @return List of allowed actions for this stage of execution, will never + * be null + */ + public List getAvailableActions() { + return new ArrayList(availableActions_); + } + + /** + * Add to the list of allowed actions for this stage of execution. + * + * @param action Action to add, can be null in which case it will be ignored + */ + public void addAvailableAction(Action action) { + if (action != null) { + availableActions_.add(action); + } + } + + /** + * @return List of information elements for the current status of the + * command, will never be null + */ + public List getNotes() { + return new ArrayList(notes_); + } + + /** + * Add to the list of information elements for the current status of the + * command. + * + * @param note Note to add, can be null in which case it will be ignored + */ + public void addNote(Note note) { + if (note != null) { + notes_.add(note); + } + } + + /** + * @return form of the command, can be null. The instance of the form stored + * in the object is returned, a copy is not made. + */ + public Form getForm() { + return form_; + } + + /** + * Set form for the command. + * + * @param payload Form for the command, can be null. The instance of the + * form is stored in the object, a copy is not made. + */ + public void setForm(Form payload) { + form_ = payload; + } + + @Override + public String toString() { + return Command.class.getSimpleName() + "\nnode: " + node_ + + "\nsession ID: " + sessionID_ + "\naction: " + action_ + + "\nstatus: " + status_ + "\nexecute action: " + + executeAction_; + } +} diff --git a/src/com/isode/stroke/elements/Form.java b/src/com/isode/stroke/elements/Form.java new file mode 100644 index 0000000..1c2e830 --- /dev/null +++ b/src/com/isode/stroke/elements/Form.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.elements; + +import java.util.ArrayList; +import java.util.List; + +import com.isode.stroke.elements.FormField.HiddenFormField; + +/** + * XEP-0004 Data form. For the relevant Fields, the parsers and serialisers + * protect the API user against the strange multi-value instead of newline thing + * by transforming them. + */ +public class Form extends Payload { + /** + * Attribute "type" + */ + public static final String FORM_ATTRIBUTE_TYPE = "type"; + + /** + * Element "title" + */ + public static final String FORM_ELEMENT_TITLE = "title"; + + /** + * Element "instructions" + */ + public static final String FORM_ELEMENT_INSTRUCTIONS = "instructions"; + + /** + * Element "field" + */ + public static final String FORM_ELEMENT_FIELD = "field"; + + /** + * Type of form + */ + public enum Type { + /** + * The form-processing entity is asking the form-submitting entity to + * complete a form. + */ + FORM_TYPE("form"), + /** + * The form-submitting entity is submitting data to the form-processing + * entity. + */ + SUBMIT_TYPE("submit"), + /** + * The form-submitting entity has cancelled submission of data to the + * form-processing entity. + */ + CANCEL_TYPE("cancel"), + /** + * The form-processing entity is returning data (e.g., search results) + * to the form-submitting entity, or the data is a generic data set. + */ + RESULT_TYPE("result"); + + private String stringForm_; + + private Type(String stringForm) { + stringForm_ = stringForm; + } + + /** + * Get type from its string form. + * + * @param stringForm String form of type, can be null + * + * @return Corresponding type if match found, otherwise + * {@link Type#FORM_TYPE}. Will never be null. + */ + public static Type getType(String stringForm) { + if (stringForm != null) { + for (Type type : Type.values()) { + if (type.stringForm_.equals(stringForm)) { + return type; + } + } + } + + return FORM_TYPE; + } + + /** + * @return String form of type, will never be null + */ + public String getStringForm() { + return stringForm_; + } + }; + + private List fields_ = new ArrayList(); + private String title_ = ""; + private String instructions_ = ""; + private Type type_; + + /** + * Create a form of the given type. + * + * @param type Form type, if null then {@link Type#FORM_TYPE} is assumed + */ + public Form(Type type) { + setType(type); + } + + /** + * Create a form of {@link Type#FORM_TYPE}. + */ + public Form() { + this(null); + } + + /** + * Add to the list of fields for the form. + * + * @param field Field to add, can be null in which case it will be ignored. + * The instance of the form field is stored in the object, a copy + * is not made. + */ + public void addField(FormField field) { + if (field != null) { + fields_.add(field); + } + } + + /** + * @return List of fields for the form, will never be null. The instances of + * the form fields stored in the object is returned, a copy is not + * made. + */ + public List getFields() { + return new ArrayList(fields_); + } + + /** + * Set title of the form. + * + * @param title title of the form, can be null in which case an empty string + * will be stored + */ + public void setTitle(String title) { + title_ = (title != null) ? title : ""; + } + + /** + * @return title of the form, will never be null + */ + public String getTitle() { + return title_; + } + + /** + * Set natural-language instructions for the form. + * + * @param instructions instructions for the form, can be null in which case + * an empty string will be stored + */ + public void setInstructions(String instructions) { + instructions_ = (instructions != null) ? instructions : ""; + } + + /** + * @return natural-language instructions for the form, will never be null + */ + public String getInstructions() { + return instructions_; + } + + /** + * Set type of the form. + * + * @param type Form type, if null then {@link Type#FORM_TYPE} is assumed + */ + public void setType(Type type) { + type_ = (type != null) ? type : Type.FORM_TYPE; + } + + /** + * @return type of the form, will never be null + */ + public Type getType() { + return type_; + } + + /** + * @return Value of the "FORM_TYPE" hidden form field if it is present in + * the form, an empty string otherwise, will never be null + */ + public String getFormType() { + String value = null; + + FormField field = getField("FORM_TYPE"); + try { + HiddenFormField f = (HiddenFormField) field; + if (f != null) { + value = f.getValue(); + } + } catch (ClassCastException e) { + // value remains null + } + + return ((value != null) ? value : ""); + } + + /** + * Get form field for the given name. + * + * @param name Name of form field to retrieve, can be null in which case + * null will be returned + * + * @return Form field with the given name if it is present in the form, null + * otherwise. The instance of the form field stored in the object is + * returned, a copy is not made. + */ + public FormField getField(String name) { + if (name != null) { + for (FormField field : fields_) { + if (field.getName().equals(name)) { + return field; + } + } + } + + return null; + } + + @Override + public String toString() { + return Form.class.getSimpleName() + "\ntitle: " + title_ + + "\ninstructions: " + instructions_ + "\ntype: " + type_; + } +} diff --git a/src/com/isode/stroke/elements/FormField.java b/src/com/isode/stroke/elements/FormField.java new file mode 100644 index 0000000..06b2ce6 --- /dev/null +++ b/src/com/isode/stroke/elements/FormField.java @@ -0,0 +1,634 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.elements; + +import java.util.ArrayList; +import java.util.List; + +import com.isode.stroke.elements.Form.Type; +import com.isode.stroke.jid.JID; + +/** + * This class implements the field element of a form. + * + *

+ * Data validation is the responsibility of the form-processing entity (commonly + * a server, service, or bot) rather than the form-submitting entity (commonly a + * client controlled by a human user). This helps to meet the requirement for + * keeping client implementations simple. If the form-processing entity + * determines that the data provided is not valid, it SHOULD return a + * "Not Acceptable" error, optionally providing a textual explanation. + */ +public class FormField { + /** + * Attribute "var" + */ + public static final String FORM_FIELD_ATTRIBUTE_VAR = "var"; + + /** + * Attribute "label" + */ + public static final String FORM_FIELD_ATTRIBUTE_LABEL = "label"; + + /** + * Element "required" + */ + public static final String FORM_FIELD_ELEMENT_REQUIRED = "required"; + + /** + * Element "desc" + */ + public static final String FORM_FIELD_ELEMENT_DESC = "desc"; + + /** + * Element "value" + */ + public static final String FORM_FIELD_ELEMENT_VALUE = "value"; + + /** + * Attribute "type" + */ + public static final String FORM_FIELD_ATTRIBUTE_TYPE = "type"; + + /** + * Element "option" + */ + public static final String FORM_FIELD_ELEMENT_OPTION = "option"; + + /** + * Attribute option "label" + */ + public static final String FORM_FIELD_ATTRIBUTE_OPTION_LABEL = "label"; + + /** + * Element option "value" + */ + public static final String FORM_FIELD_ELEMENT_OPTION_VALUE = "value"; + + /** + * Type "boolean" + */ + public static final String FORM_FIELD_TYPE_BOOLEAN = "boolean"; + + /** + * Type "fixed" + */ + public static final String FORM_FIELD_TYPE_FIXED = "fixed"; + + /** + * Type "hidden" + */ + public static final String FORM_FIELD_TYPE_HIDDEN = "hidden"; + + /** + * Type "list-single" + */ + public static final String FORM_FIELD_TYPE_LIST_SINGLE = "list-single"; + + /** + * Type "text-private" + */ + public static final String FORM_FIELD_TYPE_TEXT_PRIVATE = "text-private"; + + /** + * Type "text-single" + */ + public static final String FORM_FIELD_TYPE_TEXT_SINGLE = "text-single"; + + /** + * Type "jid-multi" + */ + public static final String FORM_FIELD_TYPE_JID_MULTI = "jid-multi"; + + /** + * Type "jid-single" + */ + public static final String FORM_FIELD_TYPE_JID_SINGLE = "jid-single"; + + /** + * Type "list-multi" + */ + public static final String FORM_FIELD_TYPE_LIST_MULTI = "list-multi"; + + /** + * Type "text-multi" + */ + public static final String FORM_FIELD_TYPE_TEXT_MULTI = "text-multi"; + + /** + * This class defines the option element that can be used in + * {@link ListSingleFormField} and {@link ListMultiFormField}. TODO: This + * class should be immutable. + */ + public static class Option { + /** + * Human-readable name for the option, must not be null + */ + public String label; + + /** + * Option value, must not be null + */ + public String value; + + /** + * Create an option element. + * + * @param label Human-readable name for the option, can be null in which + * case an empty string will be stored + * @param value Option value, must not be null + */ + public Option(String label, String value) { + if (value == null) { + throw new NullPointerException("'value' must not be null"); + } + + this.label = (label != null) ? label : ""; + this.value = value; + } + } + + private String name = ""; + private String label = ""; + private String description = ""; + private boolean required; + private List

{ + private static abstract class FieldParseHelper { + protected GenericFormField field; + + public void addValue(String s) { + field.addRawValue(s); + } + + public GenericFormField getField() { + return field; + } + } + + private static class BoolFieldParseHelper extends FieldParseHelper { + public void addValue(String s) { + field.setValue(((s.equals("1")) || (s.equals("true")))); + super.addValue(s); + } + } + + private static class StringFieldParseHelper extends + FieldParseHelper { + public void addValue(String s) { + if (field.getValue().isEmpty()) { + field.setValue(s); + } else { + field.setValue(field.getValue() + "\n" + s); + } + super.addValue(s); + } + }; + + private static class JIDFieldParseHelper extends FieldParseHelper { + public void addValue(String s) { + field.setValue(new JID(s)); + super.addValue(s); + } + }; + + private static class StringListFieldParseHelper extends + FieldParseHelper> { + public void addValue(String s) { + List l = field.getValue(); + l.add(s); + field.setValue(l); + super.addValue(s); + } + }; + + private static class JIDListFieldParseHelper extends + FieldParseHelper> { + public void addValue(String s) { + List l = field.getValue(); + l.add(new JID(s)); + field.setValue(l); + super.addValue(s); + } + }; + + private static class BooleanFormFieldParseHelper extends + BoolFieldParseHelper { + public BooleanFormFieldParseHelper() { + field = BooleanFormField.create(); + } + + public static BooleanFormFieldParseHelper create() { + return new BooleanFormFieldParseHelper(); + } + } + + private static class FixedFormFieldParseHelper extends + StringFieldParseHelper { + public FixedFormFieldParseHelper() { + field = FixedFormField.create(); + } + + public static FixedFormFieldParseHelper create() { + return new FixedFormFieldParseHelper(); + } + } + + private static class HiddenFormFieldParseHelper extends + StringFieldParseHelper { + public HiddenFormFieldParseHelper() { + field = HiddenFormField.create(); + } + + public static HiddenFormFieldParseHelper create() { + return new HiddenFormFieldParseHelper(); + } + } + + private static class ListSingleFormFieldParseHelper extends + StringFieldParseHelper { + public ListSingleFormFieldParseHelper() { + field = ListSingleFormField.create(); + } + + public static ListSingleFormFieldParseHelper create() { + return new ListSingleFormFieldParseHelper(); + } + } + + private static class TextMultiFormFieldParseHelper extends + StringFieldParseHelper { + public TextMultiFormFieldParseHelper() { + field = TextMultiFormField.create(); + } + + public static TextMultiFormFieldParseHelper create() { + return new TextMultiFormFieldParseHelper(); + } + } + + private static class TextPrivateFormFieldParseHelper extends + StringFieldParseHelper { + public TextPrivateFormFieldParseHelper() { + field = TextPrivateFormField.create(); + } + + public static TextPrivateFormFieldParseHelper create() { + return new TextPrivateFormFieldParseHelper(); + } + } + + private static class TextSingleFormFieldParseHelper extends + StringFieldParseHelper { + public TextSingleFormFieldParseHelper() { + field = TextSingleFormField.create(); + } + + public static TextSingleFormFieldParseHelper create() { + return new TextSingleFormFieldParseHelper(); + } + } + + private static class JIDSingleFormFieldParseHelper extends + JIDFieldParseHelper { + public JIDSingleFormFieldParseHelper() { + field = JIDSingleFormField.create(); + } + + public static JIDSingleFormFieldParseHelper create() { + return new JIDSingleFormFieldParseHelper(); + } + } + + private static class JIDMultiFormFieldParseHelper extends + JIDListFieldParseHelper { + public JIDMultiFormFieldParseHelper() { + field = JIDMultiFormField.create(); + } + + public static JIDMultiFormFieldParseHelper create() { + return new JIDMultiFormFieldParseHelper(); + } + } + + private static class ListMultiFormFieldParseHelper extends + StringListFieldParseHelper { + public ListMultiFormFieldParseHelper() { + field = ListMultiFormField.create(); + } + + public static ListMultiFormFieldParseHelper create() { + return new ListMultiFormFieldParseHelper(); + } + } + + private static final int TopLevel = 0; + private static final int PayloadLevel = 1; + private static final int FieldLevel = 2; + + private int level_; + private String currentText_ = ""; + private String currentOptionLabel_; + private FieldParseHelper currentFieldParseHelper_ = null; + + /** + * Constructor + */ + public FormParser() { + super(new Form()); + + level_ = TopLevel; + } + + public void handleStartElement(String element, String ns, + AttributeMap attributes) { + Form form = getPayloadInternal(); + + if (level_ == TopLevel) { + String type = attributes.getAttribute(Form.FORM_ATTRIBUTE_TYPE); + form.setType(Type.getType(type)); + } else if (level_ == PayloadLevel) { + if (element.equals(Form.FORM_ELEMENT_TITLE)) { + currentText_ = ""; + } else if (element.equals(Form.FORM_ELEMENT_INSTRUCTIONS)) { + currentText_ = ""; + } else if (element.equals(Form.FORM_ELEMENT_FIELD)) { + String type = attributes + .getAttribute(FormField.FORM_FIELD_ATTRIBUTE_TYPE); + if (type == null) { + type = ""; + } + if (type.equals(FormField.FORM_FIELD_TYPE_BOOLEAN)) { + currentFieldParseHelper_ = BooleanFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_FIXED)) { + currentFieldParseHelper_ = FixedFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_HIDDEN)) { + currentFieldParseHelper_ = HiddenFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_JID_MULTI)) { + currentFieldParseHelper_ = JIDMultiFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_JID_SINGLE)) { + currentFieldParseHelper_ = JIDSingleFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_LIST_MULTI)) { + currentFieldParseHelper_ = ListMultiFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_LIST_SINGLE)) { + currentFieldParseHelper_ = ListSingleFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_TEXT_MULTI)) { + currentFieldParseHelper_ = TextMultiFormFieldParseHelper + .create(); + } else if (type.equals(FormField.FORM_FIELD_TYPE_TEXT_PRIVATE)) { + currentFieldParseHelper_ = TextPrivateFormFieldParseHelper + .create(); + } else { + /* + * if (type == FormField.FORM_FIELD_TYPE_TEXT_SINGLE) || + * undefined + */ + currentFieldParseHelper_ = TextSingleFormFieldParseHelper + .create(); + } + + if (currentFieldParseHelper_ != null) { + String name = attributes + .getAttribute(FormField.FORM_FIELD_ATTRIBUTE_VAR); + currentFieldParseHelper_.getField().setName(name); + + String label = attributes + .getAttribute(FormField.FORM_FIELD_ATTRIBUTE_LABEL); + currentFieldParseHelper_.getField().setLabel(label); + } + } + } else if ((level_ == FieldLevel) && (currentFieldParseHelper_ != null)) { + currentText_ = ""; + if (element.equals(FormField.FORM_FIELD_ELEMENT_OPTION)) { + currentOptionLabel_ = attributes + .getAttribute(FormField.FORM_FIELD_ATTRIBUTE_OPTION_LABEL); + } + } + + ++level_; + } + + public void handleEndElement(String element, String ns) { + --level_; + Form form = getPayloadInternal(); + + if (level_ == PayloadLevel) { + if (element.equals(Form.FORM_ELEMENT_TITLE)) { + String currentTitle = form.getTitle(); + if (currentTitle.isEmpty()) { + form.setTitle(currentText_); + } else { + form.setTitle(currentTitle + "\n" + currentText_); + } + } else if (element.equals(Form.FORM_ELEMENT_INSTRUCTIONS)) { + String currentInstructions = form.getInstructions(); + if (currentInstructions.isEmpty()) { + form.setInstructions(currentText_); + } else { + form.setInstructions(currentInstructions + "\n" + + currentText_); + } + } else if (element.equals(Form.FORM_ELEMENT_FIELD)) { + if (currentFieldParseHelper_ != null) { + form.addField(currentFieldParseHelper_.getField()); + currentFieldParseHelper_ = null; + } + } + } else if ((level_ == FieldLevel) && (currentFieldParseHelper_ != null)) { + if (element.equals(FormField.FORM_FIELD_ELEMENT_REQUIRED)) { + currentFieldParseHelper_.getField().setRequired(true); + } else if (element.equals(FormField.FORM_FIELD_ELEMENT_DESC)) { + currentFieldParseHelper_.getField() + .setDescription(currentText_); + } else if (element.equals(FormField.FORM_FIELD_ELEMENT_OPTION)) { + currentFieldParseHelper_.getField().addOption( + new Option(currentOptionLabel_, currentText_)); + } else if (element.equals(FormField.FORM_FIELD_ELEMENT_VALUE)) { + currentFieldParseHelper_.addValue(currentText_); + } + } + } + + public void handleCharacterData(String text) { + currentText_ += text; + } + + @Override + public String toString() { + return FormParser.class.getSimpleName() + "\nlevel: " + level_ + + "\ncurrent text: " + currentText_ + + "\ncurrent option label: " + currentOptionLabel_; + } +} diff --git a/src/com/isode/stroke/parser/payloadparsers/FormParserFactory.java b/src/com/isode/stroke/parser/payloadparsers/FormParserFactory.java new file mode 100644 index 0000000..228c093 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/FormParserFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.Form; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.PayloadParser; +import com.isode.stroke.parser.PayloadParserFactory; + +/** + * Parser factory for {@link Form} element. + */ +public class FormParserFactory implements PayloadParserFactory { + /** + * Constructor + */ + public FormParserFactory() { + } + + public boolean canParse(String element, String ns, AttributeMap attributes) { + return ns.equals("jabber:x:data"); + } + + public PayloadParser createPayloadParser() { + return new FormParser(); + } + + @Override + public String toString() { + return FormParserFactory.class.getSimpleName(); + } +} diff --git a/src/com/isode/stroke/parser/payloadparsers/FullPayloadParserFactoryCollection.java b/src/com/isode/stroke/parser/payloadparsers/FullPayloadParserFactoryCollection.java index 9a6ca00..6114626 100644 --- a/src/com/isode/stroke/parser/payloadparsers/FullPayloadParserFactoryCollection.java +++ b/src/com/isode/stroke/parser/payloadparsers/FullPayloadParserFactoryCollection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ /* @@ -34,8 +34,9 @@ public class FullPayloadParserFactoryCollection extends PayloadParserFactoryColl addFactory(new StartSessionParserFactory()); //addFactory(new SecurityLabelParserFactory()); //addFactory(new SecurityLabelsCatalogParserFactory()); - //addFactory(new FormParserFactory()); - //addFactory(new CommandParserFactory()); + addFactory(new FormParserFactory()); + addFactory(new GenericPayloadParserFactory("command", + "http://jabber.org/protocol/commands", CommandParser.class)); //addFactery(new InBandRegistrationPayloadParserFactory()); addFactory(new SearchPayloadParserFactory()); //addFactory(new StreamInitiationParserFactory()); diff --git a/src/com/isode/stroke/serializer/payloadserializers/CommandSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/CommandSerializer.java new file mode 100644 index 0000000..411e711 --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/CommandSerializer.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Kevin Smith + * All rights reserved. + */ + +package com.isode.stroke.serializer.payloadserializers; + +import com.isode.stroke.elements.Command; +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.Command.Action; +import com.isode.stroke.elements.Command.Note; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLElement; +import com.isode.stroke.serializer.xml.XMLRawTextNode; +import com.isode.stroke.serializer.xml.XMLTextNode; + +/** + * Serializer for {@link Command} element. + */ +public class CommandSerializer extends GenericPayloadSerializer { + /** + * Constructor + */ + public CommandSerializer() { + super(Command.class); + } + + public String serializePayload(Command command) { + XMLElement commandElement = new XMLElement("command", + "http://jabber.org/protocol/commands"); + commandElement.setAttribute(Command.COMMAND_ATTRIBUTE_NODE, command + .getNode()); + + if (!command.getSessionID().isEmpty()) { + commandElement.setAttribute(Command.COMMAND_ATTRIBUTE_SESSION_ID, + command.getSessionID()); + } + + String action = actionToString(command.getAction()); + if (!action.isEmpty()) { + commandElement.setAttribute(Command.COMMAND_ATTRIBUTE_ACTION, + action); + } + + String status = command.getStatus().getStringForm(); + if (!status.isEmpty()) { + commandElement.setAttribute(Command.COMMAND_ATTRIBUTE_STATUS, + status); + } + + if (command.getAvailableActions().size() > 0) { + String actions = "<" + Command.COMMAND_ELEMENT_ACTIONS; + String executeAction = actionToString(command.getExecuteAction()); + if (!executeAction.isEmpty()) { + actions += " " + Command.COMMAND_ATTRIBUTE_EXECUTE + "='" + + executeAction + "'"; + } + actions += ">"; + for (Action act : command.getAvailableActions()) { + actions += "<" + actionToString(act) + "/>"; + } + actions += ""; + commandElement.addNode(new XMLRawTextNode(actions)); + } + + for (Note note : command.getNotes()) { + XMLElement noteElement = new XMLElement( + Command.COMMAND_ELEMENT_NOTE); + String type = note.type.getStringForm(); + noteElement.setAttribute(Command.COMMAND_ATTRIBUTE_TYPE, type); + noteElement.addNode(new XMLTextNode(note.note)); + commandElement.addNode(noteElement); + } + + Form form = command.getForm(); + if (form != null) { + FormSerializer formSerializer = new FormSerializer(); + commandElement.addNode(new XMLRawTextNode(formSerializer + .serialize(form))); + } + return commandElement.serialize(); + } + + private String actionToString(Action action) { + return action.getStringForm(); + } + + @Override + public String toString() { + return CommandSerializer.class.getSimpleName(); + } +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/FormSerializer.java b/src/com/isode/stroke/serializer/payloadserializers/FormSerializer.java new file mode 100644 index 0000000..6a3760b --- /dev/null +++ b/src/com/isode/stroke/serializer/payloadserializers/FormSerializer.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Kevin Smith + * All rights reserved. + */ + +package com.isode.stroke.serializer.payloadserializers; + +import java.util.List; + +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.FormField; +import com.isode.stroke.elements.FormField.BooleanFormField; +import com.isode.stroke.elements.FormField.FixedFormField; +import com.isode.stroke.elements.FormField.GenericFormField; +import com.isode.stroke.elements.FormField.HiddenFormField; +import com.isode.stroke.elements.FormField.JIDMultiFormField; +import com.isode.stroke.elements.FormField.JIDSingleFormField; +import com.isode.stroke.elements.FormField.ListMultiFormField; +import com.isode.stroke.elements.FormField.ListSingleFormField; +import com.isode.stroke.elements.FormField.Option; +import com.isode.stroke.elements.FormField.TextMultiFormField; +import com.isode.stroke.elements.FormField.TextPrivateFormField; +import com.isode.stroke.elements.FormField.TextSingleFormField; +import com.isode.stroke.jid.JID; +import com.isode.stroke.serializer.GenericPayloadSerializer; +import com.isode.stroke.serializer.xml.XMLElement; +import com.isode.stroke.serializer.xml.XMLTextNode; + +/** + * Serializer for {@link Form} element. + */ +public class FormSerializer extends GenericPayloadSerializer { + /** + * Constructor + */ + public FormSerializer() { + super(Form.class); + } + + public String serializePayload(Form form) { + XMLElement formElement = new XMLElement("x", "jabber:x:data"); + String type = form.getType().getStringForm(); + formElement.setAttribute(Form.FORM_ATTRIBUTE_TYPE, type); + if (!form.getTitle().isEmpty()) { + multiLineify(form.getTitle(), Form.FORM_ELEMENT_TITLE, formElement); + } + if (!form.getInstructions().isEmpty()) { + multiLineify(form.getInstructions(), + Form.FORM_ELEMENT_INSTRUCTIONS, formElement); + } + for (FormField field : form.getFields()) { + formElement.addNode(fieldToXML(field)); + } + return formElement.serialize(); + } + + private XMLElement fieldToXML(FormField field) { + XMLElement fieldElement = new XMLElement(Form.FORM_ELEMENT_FIELD); + if (!field.getName().isEmpty()) { + fieldElement.setAttribute(FormField.FORM_FIELD_ATTRIBUTE_VAR, field + .getName()); + } + if (!field.getLabel().isEmpty()) { + fieldElement.setAttribute(FormField.FORM_FIELD_ATTRIBUTE_LABEL, + field.getLabel()); + } + if (field.getRequired()) { + fieldElement.addNode(new XMLElement( + FormField.FORM_FIELD_ELEMENT_REQUIRED)); + } + if (!field.getDescription().isEmpty()) { + XMLElement descriptionElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_DESC); + descriptionElement.addNode(new XMLTextNode(field.getDescription())); + fieldElement.addNode(descriptionElement); + } + + // Set the value and type + String fieldType = ""; + if (field instanceof BooleanFormField) { + fieldType = FormField.FORM_FIELD_TYPE_BOOLEAN; + XMLElement valueElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_VALUE); + valueElement.addNode(XMLTextNode.create(((BooleanFormField) field) + .getValue() ? "1" : "0")); + fieldElement.addNode(valueElement); + } else if (field instanceof FixedFormField) { + fieldType = FormField.FORM_FIELD_TYPE_FIXED; + serializeValueAsString((FixedFormField) field, fieldElement); + } else if (field instanceof HiddenFormField) { + fieldType = FormField.FORM_FIELD_TYPE_HIDDEN; + serializeValueAsString((HiddenFormField) field, fieldElement); + } else if (field instanceof ListSingleFormField) { + fieldType = FormField.FORM_FIELD_TYPE_LIST_SINGLE; + serializeValueAsString((ListSingleFormField) field, fieldElement); + } else if (field instanceof TextPrivateFormField) { + fieldType = FormField.FORM_FIELD_TYPE_TEXT_PRIVATE; + serializeValueAsString((TextPrivateFormField) field, fieldElement); + } else if (field instanceof TextSingleFormField) { + fieldType = FormField.FORM_FIELD_TYPE_TEXT_SINGLE; + serializeValueAsString((TextSingleFormField) field, fieldElement); + } else if (field instanceof JIDMultiFormField) { + fieldType = FormField.FORM_FIELD_TYPE_JID_MULTI; + List jids = ((JIDMultiFormField) (field)).getValue(); + for (JID jid : jids) { + XMLElement valueElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_VALUE); + valueElement.addNode(XMLTextNode.create(jid.toString())); + fieldElement.addNode(valueElement); + } + } else if (field instanceof JIDSingleFormField) { + fieldType = FormField.FORM_FIELD_TYPE_JID_SINGLE; + XMLElement valueElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_VALUE); + JIDSingleFormField jidSingleFormField = (JIDSingleFormField) field; + valueElement.addNode(XMLTextNode.create(jidSingleFormField + .getValue().toString())); + fieldElement.addNode(valueElement); + } else if (field instanceof ListMultiFormField) { + fieldType = FormField.FORM_FIELD_TYPE_LIST_MULTI; + List lines = ((ListMultiFormField) (field)).getValue(); + for (String line : lines) { + XMLElement valueElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_VALUE); + valueElement.addNode(XMLTextNode.create(line)); + fieldElement.addNode(valueElement); + } + } else if (field instanceof TextMultiFormField) { + fieldType = FormField.FORM_FIELD_TYPE_TEXT_MULTI; + multiLineify(((TextMultiFormField) field).getValue(), + FormField.FORM_FIELD_ELEMENT_VALUE, fieldElement); + } else { + assert (false); + } + + if (!fieldType.isEmpty()) { + fieldElement.setAttribute(FormField.FORM_FIELD_ATTRIBUTE_TYPE, + fieldType); + } + + for (Option option : field.getOptions()) { + XMLElement optionElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_OPTION); + if (!option.label.isEmpty()) { + optionElement.setAttribute( + FormField.FORM_FIELD_ATTRIBUTE_OPTION_LABEL, + option.label); + } + + XMLElement valueElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_OPTION_VALUE); + valueElement.addNode(XMLTextNode.create(option.value)); + optionElement.addNode(valueElement); + + fieldElement.addNode(optionElement); + } + + return fieldElement; + } + + private void multiLineify(String text, String elementName, + XMLElement element) { + String unRdText = text.replaceAll("\r", ""); + String[] lines = unRdText.split("\n"); + for (String line : lines) { + XMLElement lineElement = new XMLElement(elementName); + lineElement.addNode(new XMLTextNode(line)); + element.addNode(lineElement); + } + } + + private static void serializeValueAsString(GenericFormField field, + XMLElement parent) { + String value = field.getValue(); + if (!value.isEmpty()) { + XMLElement valueElement = new XMLElement( + FormField.FORM_FIELD_ELEMENT_VALUE); + valueElement.addNode(XMLTextNode.create(value)); + parent.addNode(valueElement); + } + } + + @Override + public String toString() { + return FormSerializer.class.getSimpleName(); + } +} diff --git a/src/com/isode/stroke/serializer/payloadserializers/FullPayloadSerializerCollection.java b/src/com/isode/stroke/serializer/payloadserializers/FullPayloadSerializerCollection.java index ddd3391..f43a47e 100644 --- a/src/com/isode/stroke/serializer/payloadserializers/FullPayloadSerializerCollection.java +++ b/src/com/isode/stroke/serializer/payloadserializers/FullPayloadSerializerCollection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ /* @@ -41,9 +41,9 @@ public class FullPayloadSerializerCollection extends PayloadSerializerCollection addSerializer(new RawXMLPayloadSerializer()); //addSerializer(new StorageSerializer()); //addSerializer(new DelaySerializer()); - //addSerializer(new FormSerializer()); + addSerializer(new FormSerializer()); //addSerializer(new PrivateStorageSerializer(this)); - //addSerializer(new CommandSerializer()); + addSerializer(new CommandSerializer()); //addSerializer(new NicknameSerializer()); addSerializer(new SearchPayloadSerializer()); addSerializer(new LastSerializer()); diff --git a/src/com/isode/stroke/serializer/xml/XMLElement.java b/src/com/isode/stroke/serializer/xml/XMLElement.java index b464242..43abfea 100644 --- a/src/com/isode/stroke/serializer/xml/XMLElement.java +++ b/src/com/isode/stroke/serializer/xml/XMLElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ /* @@ -8,13 +8,14 @@ */ package com.isode.stroke.serializer.xml; -import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; import java.util.Vector; public class XMLElement implements XMLNode { private final String tag_; - private final HashMap attributes_ = new HashMap(); + private final Map attributes_ = new TreeMap(); private final Vector childNodes_ = new Vector(); public XMLElement(String tag) { diff --git a/src/com/isode/stroke/serializer/xml/XMLTextNode.java b/src/com/isode/stroke/serializer/xml/XMLTextNode.java index ae5cd32..22a6785 100644 --- a/src/com/isode/stroke/serializer/xml/XMLTextNode.java +++ b/src/com/isode/stroke/serializer/xml/XMLTextNode.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ /* @@ -22,4 +22,15 @@ public class XMLTextNode implements XMLNode { public String serialize() { return text_; } + + /** + * Create new object. + * + * @param text Text to create object with, must not be null + * + * @return new object, will never be null + */ + public static XMLTextNode create(String text) { + return new XMLTextNode(text); + } } diff --git a/test/com/isode/stroke/elements/FormTest.java b/test/com/isode/stroke/elements/FormTest.java new file mode 100644 index 0000000..1d37c86 --- /dev/null +++ b/test/com/isode/stroke/elements/FormTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.elements; + +import static org.junit.Assert.assertEquals; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.isode.stroke.elements.FormField.FixedFormField; +import com.isode.stroke.elements.FormField.HiddenFormField; + +public class FormTest { + @BeforeClass + public static void init() throws Exception { + } + + @Test + public void testGetFormType() { + Form form = new Form(); + + form.addField(FixedFormField.create("Foo")); + + FormField field = HiddenFormField.create("jabber:bot"); + field.setName("FORM_TYPE"); + form.addField(field); + + form.addField(FixedFormField.create("Bar")); + + assertEquals("jabber:bot", form.getFormType()); + } + + @Test + public void testGetFormType_InvalidFormType() { + Form form = new Form(); + + FormField field = FixedFormField.create("jabber:bot"); + field.setName("FORM_TYPE"); + form.addField(field); + + assertEquals("", form.getFormType()); + } + + @Test + public void testGetFormType_NoFormType() { + Form form = new Form(); + + form.addField(FixedFormField.create("Foo")); + + assertEquals("", form.getFormType()); + } +} diff --git a/test/com/isode/stroke/parser/payloadparsers/CommandParserTest.java b/test/com/isode/stroke/parser/payloadparsers/CommandParserTest.java new file mode 100644 index 0000000..4c9854b --- /dev/null +++ b/test/com/isode/stroke/parser/payloadparsers/CommandParserTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.isode.stroke.elements.Command; +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.Payload; +import com.isode.stroke.elements.Command.Action; +import com.isode.stroke.elements.Command.Note; +import com.isode.stroke.elements.Command.Status; +import com.isode.stroke.elements.Command.Note.Type; +import com.isode.stroke.eventloop.DummyEventLoop; + +public class CommandParserTest { + private static DummyEventLoop eventLoop; + + @BeforeClass + public static void init() throws Exception { + eventLoop = new DummyEventLoop(); + } + + private static Command parse(String xmlString) { + PayloadsParserTester parser = new PayloadsParserTester(eventLoop); + assertTrue(parser.parse(xmlString)); + + Payload payload = null; + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + eventLoop.processEvents(); + payload = parser.getPayload(); + } while (payload == null); + + return (Command) payload; + } + + @Test + public void testParse() { + Command payload = parse(""); + + assertEquals(Action.PREV, payload.getAction()); + assertEquals("list", payload.getNode()); + assertEquals("myid", payload.getSessionID()); + } + + @Test + public void testParse_Result() { + Command payload = parse("" + + "Service 'httpd' has been configured." + + "I lied." + + "" + + "" + + "" + + "" + ""); + + assertEquals(Status.COMPLETED, payload.getStatus()); + + List notes = payload.getNotes(); + assertEquals(2, notes.size()); + assertEquals(Type.WARN, notes.get(0).type); + assertEquals("Service 'httpd' has been configured.", notes.get(0).note); + assertEquals(Type.ERROR, notes.get(1).type); + assertEquals("I lied.", notes.get(1).note); + List actions = payload.getAvailableActions(); + assertEquals(2, actions.size()); + assertEquals(Action.PREV, actions.get(0)); + assertEquals(Action.NEXT, actions.get(1)); + assertEquals(Action.NEXT, payload.getExecuteAction()); + } + + @Test + public void testParse_Form() { + Command payload = parse("" + + "" + + "Bot Configuration" + + "Hello!" + + "Fill out this form to configure your new bot!" + + "" + ""); + + Form form = payload.getForm(); + assertEquals("Bot Configuration", form.getTitle()); + assertEquals("Hello!\nFill out this form to configure your new bot!", + form.getInstructions()); + assertEquals(Form.Type.RESULT_TYPE, form.getType()); + } +} diff --git a/test/com/isode/stroke/parser/payloadparsers/FormParserTest.java b/test/com/isode/stroke/parser/payloadparsers/FormParserTest.java new file mode 100644 index 0000000..b57e7d8 --- /dev/null +++ b/test/com/isode/stroke/parser/payloadparsers/FormParserTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.Payload; +import com.isode.stroke.elements.Form.Type; +import com.isode.stroke.elements.FormField.BooleanFormField; +import com.isode.stroke.elements.FormField.FixedFormField; +import com.isode.stroke.elements.FormField.HiddenFormField; +import com.isode.stroke.elements.FormField.JIDMultiFormField; +import com.isode.stroke.elements.FormField.ListMultiFormField; +import com.isode.stroke.elements.FormField.ListSingleFormField; +import com.isode.stroke.elements.FormField.TextMultiFormField; +import com.isode.stroke.elements.FormField.TextSingleFormField; +import com.isode.stroke.eventloop.DummyEventLoop; +import com.isode.stroke.jid.JID; + +public class FormParserTest { + private static DummyEventLoop eventLoop; + + @BeforeClass + public static void init() throws Exception { + eventLoop = new DummyEventLoop(); + } + + private static Form parse(String xmlString) { + PayloadsParserTester parser = new PayloadsParserTester(eventLoop); + assertTrue(parser.parse(xmlString)); + + Payload payload = null; + do { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + eventLoop.processEvents(); + payload = parser.getPayload(); + } while (payload == null); + + return (Form) payload; + } + + @Test + public void testParse_FormInformation() throws Exception { + Form payload = parse("" + + "Bot Configuration" + + "Hello!" + + "Fill out this form to configure your new bot!" + + ""); + assertEquals("Bot Configuration", payload.getTitle()); + assertEquals("Hello!\nFill out this form to configure your new bot!", + payload.getInstructions()); + assertEquals(Type.SUBMIT_TYPE, payload.getType()); + } + + @Test + public void testParse() { + Form payload = parse("" + + "" + + "jabber:bot" + + "" + + "Section 1: Bot Info" + + "" + + "This is a bot.A quite good one actually" + + "" + + "" + + "1" + + "" + + "" + + "" + + "news" + + "search" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "20" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "Tell all your friends about your new bot!" + + "foo@bar.com" + "baz@fum.org" + + "" + "" + "foo" + + "" + ""); + + assertEquals(10, payload.getFields().size()); + assertEquals("jabber:bot", ((HiddenFormField) (payload.getFields() + .get(0))).getValue()); + assertEquals("FORM_TYPE", payload.getFields().get(0).getName()); + assertTrue(!payload.getFields().get(0).getRequired()); + + assertEquals("Section 1: Bot Info", ((FixedFormField) (payload + .getFields().get(1))).getValue()); + + assertEquals("The name of your bot", payload.getFields().get(2) + .getLabel()); + + assertEquals("This is a bot.\nA quite good one actually", + ((TextMultiFormField) (payload.getFields().get(3))).getValue()); + + assertEquals(Boolean.TRUE, ((BooleanFormField) (payload.getFields() + .get(4))).getValue()); + assertTrue(payload.getFields().get(4).getRequired()); + assertEquals("1", ((BooleanFormField) (payload.getFields().get(4))) + .getRawValues().get(0)); + + assertEquals("news", + ((ListMultiFormField) (payload.getFields().get(6))).getValue() + .get(0)); + assertEquals("news", payload.getFields().get(6).getRawValues().get(0)); + assertEquals("search", ((ListMultiFormField) (payload.getFields() + .get(6))).getValue().get(1)); + assertEquals("search", payload.getFields().get(6).getRawValues().get(1)); + assertEquals(5, payload.getFields().get(6).getOptions().size()); + assertEquals("Contests", + payload.getFields().get(6).getOptions().get(0).label); + assertEquals("contests", + payload.getFields().get(6).getOptions().get(0).value); + assertEquals("News", + payload.getFields().get(6).getOptions().get(1).label); + assertEquals("news", + payload.getFields().get(6).getOptions().get(1).value); + + assertEquals("20", ((ListSingleFormField) (payload.getFields().get(7))) + .getValue()); + + assertEquals(new JID("foo@bar.com"), ((JIDMultiFormField) (payload + .getFields().get(8))).getValue().get(0)); + assertEquals(new JID("baz@fum.org"), ((JIDMultiFormField) (payload + .getFields().get(8))).getValue().get(1)); + assertEquals("Tell all your friends about your new bot!", payload + .getFields().get(8).getDescription()); + + assertEquals("foo", + ((TextSingleFormField) (payload.getFields().get(9))).getValue()); + } +} \ No newline at end of file diff --git a/test/com/isode/stroke/parser/payloadparsers/PayloadsParserTester.java b/test/com/isode/stroke/parser/payloadparsers/PayloadsParserTester.java new file mode 100644 index 0000000..d5614fb --- /dev/null +++ b/test/com/isode/stroke/parser/payloadparsers/PayloadsParserTester.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.parser.payloadparsers; + +import com.isode.stroke.elements.Payload; +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.PayloadParser; +import com.isode.stroke.parser.PayloadParserFactory; +import com.isode.stroke.parser.PlatformXMLParserFactory; +import com.isode.stroke.parser.XMLParser; +import com.isode.stroke.parser.XMLParserClient; + +public class PayloadsParserTester implements XMLParserClient { + private XMLParser xmlParser; + private FullPayloadParserFactoryCollection factories = new FullPayloadParserFactoryCollection(); + private PayloadParser payloadParser; + private int level; + + public PayloadsParserTester(EventLoop eventLoop) { + level = 0; + xmlParser = PlatformXMLParserFactory.createXMLParser(this, eventLoop); + } + + public boolean parse(String data) { + return xmlParser.parse(data); + } + + public void handleStartElement(String element, String ns, + AttributeMap attributes) { + if (level == 0) { + assert (payloadParser == null); + PayloadParserFactory payloadParserFactory = factories + .getPayloadParserFactory(element, ns, attributes); + assert (payloadParserFactory != null); + payloadParser = payloadParserFactory.createPayloadParser(); + } + payloadParser.handleStartElement(element, ns, attributes); + level++; + } + + public void handleEndElement(String element, String ns) { + level--; + payloadParser.handleEndElement(element, ns); + } + + public void handleCharacterData(String data) { + payloadParser.handleCharacterData(data); + } + + public Payload getPayload() { + if (payloadParser == null) { + return null; + } + return payloadParser.getPayload(); + } +} diff --git a/test/com/isode/stroke/serializer/payloadserializers/FormSerializerTest.java b/test/com/isode/stroke/serializer/payloadserializers/FormSerializerTest.java new file mode 100644 index 0000000..4a1ccc0 --- /dev/null +++ b/test/com/isode/stroke/serializer/payloadserializers/FormSerializerTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.serializer.payloadserializers; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.isode.stroke.elements.Form; +import com.isode.stroke.elements.FormField; +import com.isode.stroke.elements.Form.Type; +import com.isode.stroke.elements.FormField.BooleanFormField; +import com.isode.stroke.elements.FormField.FixedFormField; +import com.isode.stroke.elements.FormField.HiddenFormField; +import com.isode.stroke.elements.FormField.JIDMultiFormField; +import com.isode.stroke.elements.FormField.ListMultiFormField; +import com.isode.stroke.elements.FormField.ListSingleFormField; +import com.isode.stroke.elements.FormField.Option; +import com.isode.stroke.elements.FormField.TextMultiFormField; +import com.isode.stroke.elements.FormField.TextPrivateFormField; +import com.isode.stroke.elements.FormField.TextSingleFormField; +import com.isode.stroke.jid.JID; + +public class FormSerializerTest { + @BeforeClass + public static void init() throws Exception { + } + + @Test + public void testSerializeFormInformation() { + FormSerializer testling = new FormSerializer(); + Form form = new Form(Type.FORM_TYPE); + form.setTitle("Bot Configuration"); + form + .setInstructions("Hello!\nFill out this form to configure your new bot!"); + + assertEquals( + "" + + "Bot Configuration" + + "Hello!" + + "Fill out this form to configure your new bot!" + + "", testling.serialize(form)); + } + + @Test + public void testSerializeFields() { + FormSerializer testling = new FormSerializer(); + Form form = new Form(Type.FORM_TYPE); + + FormField field = HiddenFormField.create("jabber:bot"); + field.setName("FORM_TYPE"); + form.addField(field); + + form.addField(FixedFormField.create("Section 1: Bot Info")); + + field = TextSingleFormField.create(); + field.setName("botname"); + field.setLabel("The name of your bot"); + form.addField(field); + + field = TextMultiFormField + .create("This is a bot.\nA quite good one actually"); + field.setName("description"); + field.setLabel("Helpful description of your bot"); + form.addField(field); + + field = BooleanFormField.create(true); + field.setName("public"); + field.setLabel("Public bot?"); + field.setRequired(true); + form.addField(field); + + field = TextPrivateFormField.create(); + field.setName("password"); + field.setLabel("Password for special access"); + form.addField(field); + + List values = new ArrayList(); + values.add("news"); + values.add("search"); + field = ListMultiFormField.create(values); + field.setName("features"); + field.setLabel("What features will the bot support?"); + field.addOption(new Option("Contests", "contests")); + field.addOption(new Option("News", "news")); + field.addOption(new Option("Polls", "polls")); + field.addOption(new Option("Reminders", "reminders")); + field.addOption(new Option("Search", "search")); + form.addField(field); + + field = ListSingleFormField.create("20"); + field.setName("maxsubs"); + field.setLabel("Maximum number of subscribers"); + field.addOption(new Option("10", "10")); + field.addOption(new Option("20", "20")); + field.addOption(new Option("30", "30")); + field.addOption(new Option("50", "50")); + field.addOption(new Option("100", "100")); + field.addOption(new Option("", "none")); + form.addField(field); + + List jids = new ArrayList(); + jids.add(new JID("foo@bar.com")); + jids.add(new JID("baz@fum.org")); + field = JIDMultiFormField.create(jids); + field.setName("invitelist"); + field.setLabel("People to invite"); + field.setDescription("Tell all your friends about your new bot!"); + form.addField(field); + + assertEquals( + "" + + "" + + "jabber:bot" + + "" + + "Section 1: Bot Info" + + "" + + "This is a bot.A quite good one actually" + + "" + + "" + + "1" + + "" + + "" + + "" + + "news" + + "search" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "20" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "Tell all your friends about your new bot!" + + "foo@bar.com" + + "baz@fum.org" + "" + "", + testling.serialize(form)); + } +} -- cgit v0.10.2-6-g49f6