diff options
author | Mili Verma <mili.verma@isode.com> | 2012-01-06 15:00:55 (GMT) |
---|---|---|
committer | Kevin Smith <git@kismith.co.uk> | 2012-01-09 15:54:52 (GMT) |
commit | cc760bfd15caadb56bfef477cb54dc94c25f7fa7 (patch) | |
tree | 05f5918b97488b05ca1266c0644a3874adb98129 /src/com/isode | |
parent | 12b1d667965556002ea0fd300a71bcdf57634e90 (diff) | |
download | stroke-cc760bfd15caadb56bfef477cb54dc94c25f7fa7.zip stroke-cc760bfd15caadb56bfef477cb54dc94c25f7fa7.tar.bz2 |
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.
Diffstat (limited to 'src/com/isode')
15 files changed, 2489 insertions, 12 deletions
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<Command> onNextStageReceived = new Signal1<Command>(); + + /** + * 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<ErrorPayload> onError = new Signal1<ErrorPayload>(); + + private JID to_; + private String commandNode_; + private IQRouter iqRouter_; + private boolean isMultiStage_; + private String sessionID_; + private HashMap<Action, ActionState> actionStates_ = new HashMap<Action, ActionState>(); + + /** + * 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<Action> 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<Command> commandRequest = new GenericRequest<Command>( + IQ.Type.Set, to_, new Command(commandNode_, null, action), + iqRouter_); + commandRequest.onResponse.connect(new Slot2<Command, ErrorPayload>() { + 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<Command> commandRequest = new GenericRequest<Command>( + IQ.Type.Set, to_, command, iqRouter_); + commandRequest.onResponse.connect(new Slot2<Command, ErrorPayload>() { + 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. + * + * <p> + * 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<Action> availableActions_ = new ArrayList<Action>(); + private List<Note> notes_ = new ArrayList<Note>(); + 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<Action> getAvailableActions() { + return new ArrayList<Action>(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<Note> getNotes() { + return new ArrayList<Note>(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<FormField> fields_ = new ArrayList<FormField>(); + 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<FormField> getFields() { + return new ArrayList<FormField>(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. + * + * <p> + * 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<Option> options = new ArrayList<Option>(); + private List<String> rawValues = new ArrayList<String>(); + + protected FormField() { + required = false; + } + + /** + * Set the unique identifier for the field in the form. + * + * @param name unique identifier for the field in the form, can be null in + * which case an empty string will be stored + */ + public void setName(String name) { + this.name = (name != null) ? name : ""; + } + + /** + * @return unique identifier for the field, will never be null + */ + public String getName() { + return name; + } + + /** + * Set the human-readable name for the field. + * + * @param label human-readable name for the field, can be null in which case + * an empty string will be stored + */ + public void setLabel(String label) { + this.label = (label != null) ? label : ""; + } + + /** + * @return human-readable name for the field, will never be null + */ + public String getLabel() { + return label; + } + + /** + * Set the natural-language description for the field. + * + * @param description natural-language description for the field, can be + * null in which case an empty string will be stored + */ + public void setDescription(String description) { + this.description = (description != null) ? description : ""; + } + + /** + * @return natural-language description for the field, will never be null + */ + public String getDescription() { + return description; + } + + /** + * Set if the field is required for the form to be considered valid. + * + * @param required true if the field is required for the form to be + * considered valid + */ + public void setRequired(boolean required) { + this.required = required; + } + + /** + * @return true if the field is required for the form to be considered valid + */ + public boolean getRequired() { + return required; + } + + /** + * Add to the list of options for this form field. + * + * @param option Option to add, can be null in which case it will be ignored + */ + public void addOption(Option option) { + if (option != null) { + options.add(option); + } + } + + /** + * @return List of options for this form, will never be null + */ + public List<Option> getOptions() { + return new ArrayList<Option>(options); + } + + /** + * Add to values for this field. The values can be the defaults suggested in + * a form of {@link Type#FORM_TYPE} or results provided in a form of + * {@link Type#RESULT_TYPE} or values submitted in a form of + * {@link Type#SUBMIT_TYPE}. + * + * @param value Value to add, can be null + */ + public void addRawValue(String value) { + rawValues.add(value); + } + + /** + * @return List of values for this field, will never be null + */ + public List<String> getRawValues() { + return new ArrayList<String>(rawValues); + } + + /** + * Template for creating a form field. + * + * @param <T> Type of form field. + */ + public static class GenericFormField<T> extends FormField { + private T value; + + /** + * @return Values for this field. The values can be the defaults + * suggested in a form of {@link Type#FORM_TYPE} or results + * provided in a form of {@link Type#RESULT_TYPE} or values + * submitted in a form of {@link Type#SUBMIT_TYPE}. Will never be + * null. + */ + public T getValue() { + return value; + } + + /** + * Set values for this field. The values can be the defaults suggested + * in a form of {@link Type#FORM_TYPE} or results provided in a form of + * {@link Type#RESULT_TYPE} or values submitted in a form of + * {@link Type#SUBMIT_TYPE}. + * + * @param value Value to set, must not be null + */ + public void setValue(T value) { + if (value == null) { + throw new NullPointerException("'value' must not be null"); + } + + this.value = value; + } + + protected GenericFormField(T value) { + setValue(value); + } + } + + /** + * This field enables an entity to gather or provide an either-or choice + * between two options. The default value is "false". + */ + public static class BooleanFormField extends GenericFormField<Boolean> { + private BooleanFormField(Boolean value) { + super((value == null) ? Boolean.FALSE : value); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be assumed + * as FALSE. + * + * @return new object, will never be null + */ + public static BooleanFormField create(Boolean value) { + return new BooleanFormField(value); + } + + /** + * Create an object with value FALSE. + * + * @return new object, will never be null + */ + public static BooleanFormField create() { + return create(null); + } + } + + /** + * This field is intended for data description (e.g., human-readable text + * such as "section" headers) rather than data gathering or provision. + */ + public static class FixedFormField extends GenericFormField<String> { + private FixedFormField(String value) { + super((value != null) ? value : ""); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be stored + * as empty string. + * + * @return new object, will never be null + */ + public static FixedFormField create(String value) { + return new FixedFormField(value); + } + + /** + * Create an object with value as an empty string. + * + * @return new object, will never be null + */ + public static FixedFormField create() { + return create(null); + } + } + + /** + * This field is not shown to the form-submitting entity, but instead is + * returned with the form. + */ + public static class HiddenFormField extends GenericFormField<String> { + private HiddenFormField(String value) { + super((value != null) ? value : ""); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be stored + * as empty string. + * + * @return new object, will never be null + */ + public static HiddenFormField create(String value) { + return new HiddenFormField(value); + } + + /** + * Create an object with value as an empty string. + * + * @return new object, will never be null + */ + public static HiddenFormField create() { + return create(null); + } + } + + /** + * This field enables an entity to gather or provide one option from among + * many. + */ + public static class ListSingleFormField extends GenericFormField<String> { + private ListSingleFormField(String value) { + super((value != null) ? value : ""); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be stored + * as empty string. + * + * @return new object, will never be null + */ + public static ListSingleFormField create(String value) { + return new ListSingleFormField(value); + } + + /** + * Create an object with value as an empty string. + * + * @return new object, will never be null + */ + public static ListSingleFormField create() { + return create(null); + } + } + + /** + * This field enables an entity to gather or provide multiple lines of text + * (i.e. containing newlines). + */ + public static class TextMultiFormField extends GenericFormField<String> { + private TextMultiFormField(String value) { + super((value != null) ? value : ""); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be stored + * as empty string. + * + * @return new object, will never be null + */ + public static TextMultiFormField create(String value) { + return new TextMultiFormField(value); + } + + /** + * Create an object with value as an empty string. + * + * @return new object, will never be null + */ + public static TextMultiFormField create() { + return create(null); + } + } + + /** + * This field enables an entity to gather or provide a single line or word + * of text, which shall be obscured in an interface (e.g., with multiple + * instances of the asterisk character). + */ + public static class TextPrivateFormField extends GenericFormField<String> { + private TextPrivateFormField(String value) { + super((value != null) ? value : ""); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be stored + * as empty string. + * + * @return new object, will never be null + */ + public static TextPrivateFormField create(String value) { + return new TextPrivateFormField(value); + } + + /** + * Create an object with value as an empty string. + * + * @return new object, will never be null + */ + public static TextPrivateFormField create() { + return create(null); + } + } + + /** + * This field enables an entity to gather or provide a single line or word + * of text, which may be shown in an interface. + */ + public static class TextSingleFormField extends GenericFormField<String> { + private TextSingleFormField(String value) { + super((value != null) ? value : ""); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be stored + * as empty string. + * + * @return new object, will never be null + */ + public static TextSingleFormField create(String value) { + return new TextSingleFormField(value); + } + + /** + * Create an object with value as an empty string. + * + * @return new object, will never be null + */ + public static TextSingleFormField create() { + return create(null); + } + } + + /** + * This field enables an entity to gather or provide a single Jabber ID. + */ + public static class JIDSingleFormField extends GenericFormField<JID> { + private JIDSingleFormField(JID value) { + super((value != null) ? value : new JID()); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be stored + * as an invalid JID. + * + * @return new object, will never be null + */ + public static JIDSingleFormField create(JID value) { + return new JIDSingleFormField(value); + } + + /** + * Create an object with value as an invalid JID. + * + * @return new object, will never be null + */ + public static JIDSingleFormField create() { + return create(null); + } + } + + /** + * This field enables an entity to gather or provide multiple Jabber IDs. + */ + public static class JIDMultiFormField extends GenericFormField<List<JID>> { + private JIDMultiFormField(List<JID> value) { + super((value == null) ? new ArrayList<JID>() : new ArrayList<JID>( + value)); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be assumed + * to be an empty list. A copy of the given list will be kept + * by this object. + * + * @return new object, will never be null + */ + public static JIDMultiFormField create(List<JID> value) { + return new JIDMultiFormField(value); + } + + /** + * Create an object with an empty list. + * + * @return new object, will never be null + */ + public static JIDMultiFormField create() { + return create(null); + } + } + + /** + * This field enables an entity to gather or provide one or more options + * from among many. The order of items MAY be significant. + */ + public static class ListMultiFormField extends + GenericFormField<List<String>> { + private ListMultiFormField(List<String> value) { + super((value == null) ? new ArrayList<String>() + : new ArrayList<String>(value)); + } + + /** + * Create an object with given value. + * + * @param value Value for this field, can be null which will be assumed + * to be an empty list. A copy of the given list will be kept + * by this object. + * + * @return new object, will never be null + */ + public static ListMultiFormField create(List<String> value) { + return new ListMultiFormField(value); + } + + /** + * Create an object with an empty list. + * + * @return new object, will never be null + */ + public static ListMultiFormField create() { + return create(null); + } + } + + @Override + public String toString() { + return FormField.class.getSimpleName() + "\nname: " + name + + "\nlabel: " + label + "\ndescription: " + description + + "\nrequired: " + required; + } +} diff --git a/src/com/isode/stroke/eventloop/DummyEventLoop.java b/src/com/isode/stroke/eventloop/DummyEventLoop.java new file mode 100644 index 0000000..47f8928 --- /dev/null +++ b/src/com/isode/stroke/eventloop/DummyEventLoop.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012 Isode Limited, London, England. + * All rights reserved. + */ +/* + * Copyright (c) 2010 Remko Tronçon + * All rights reserved. + */ + +package com.isode.stroke.eventloop; + +import java.util.Vector; + +/** + * Dummy event loop that can be used for tests, etc. + * + * @since 15.2 + */ +public class DummyEventLoop extends EventLoop { + private Vector<Event> events_ = new Vector<Event>(); + + /** + * Constructor + */ + public DummyEventLoop() { + } + + /** + * Process pending events. + */ + public void processEvents() { + while (!events_.isEmpty()) { + handleEvent(events_.get(0)); + events_.remove(0); + } + } + + /** + * @return TRUE if there are any pending events. + */ + public boolean hasEvents() { + return !events_.isEmpty(); + } + + public void post(Event event) { + events_.add(event); + } +} diff --git a/src/com/isode/stroke/parser/PlatformXMLParserFactory.java b/src/com/isode/stroke/parser/PlatformXMLParserFactory.java index f5252a9..c1e44d6 100644 --- a/src/com/isode/stroke/parser/PlatformXMLParserFactory.java +++ b/src/com/isode/stroke/parser/PlatformXMLParserFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ /* @@ -11,7 +11,7 @@ package com.isode.stroke.parser; import com.isode.stroke.eventloop.EventLoop; -class PlatformXMLParserFactory { +public class PlatformXMLParserFactory { /** * Unlike Swiften, this may be threaded, and therefore needs an eventloop. */ diff --git a/src/com/isode/stroke/parser/payloadparsers/CommandParser.java b/src/com/isode/stroke/parser/payloadparsers/CommandParser.java new file mode 100644 index 0000000..b94acfb --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/CommandParser.java @@ -0,0 +1,142 @@ +/* + * 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.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.elements.Command.Status; +import com.isode.stroke.elements.Command.Note.Type; +import com.isode.stroke.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +/** + * Parser for {@link Command} element. + */ +public class CommandParser extends GenericPayloadParser<Command> { + private static final int TopLevel = 0; + private static final int PayloadLevel = 1; + private static final int FormOrNoteOrActionsLevel = 2; + private static final int ActionsActionLevel = 3; + + private int level_; + private boolean inNote_; + private boolean inActions_; + private Type noteType_; + private FormParserFactory formParserFactory_; + private FormParser formParser_; + private String currentText_; + + /** + * Constructor + */ + public CommandParser() { + super(new Command()); + + level_ = TopLevel; + inNote_ = false; + inActions_ = false; + noteType_ = Type.INFO; + formParser_ = null; + + formParserFactory_ = new FormParserFactory(); + } + + public void handleStartElement(String element, String ns, + AttributeMap attributes) { + ++level_; + Command command = getPayloadInternal(); + + if (level_ == PayloadLevel) { + Action action = parseAction(attributes + .getAttribute(Command.COMMAND_ATTRIBUTE_ACTION)); + command.setAction(action); + + String status = attributes + .getAttribute(Command.COMMAND_ATTRIBUTE_STATUS); + command.setStatus(Status.getStatus(status)); + + command.setNode(attributes + .getAttribute(Command.COMMAND_ATTRIBUTE_NODE)); + command.setSessionID(attributes + .getAttribute(Command.COMMAND_ATTRIBUTE_SESSION_ID)); + } else if (level_ == FormOrNoteOrActionsLevel) { + assert (formParser_ == null); + if (formParserFactory_.canParse(element, ns, attributes)) { + formParser_ = (FormParser) (formParserFactory_ + .createPayloadParser()); + assert (formParser_ != null); + } else if (element.equals(Command.COMMAND_ELEMENT_NOTE)) { + inNote_ = true; + currentText_ = ""; + String noteType = attributes + .getAttribute(Command.COMMAND_ATTRIBUTE_TYPE); + noteType_ = Type.getType(noteType); + } else if (element.equals(Command.COMMAND_ELEMENT_ACTIONS)) { + inActions_ = true; + Action action = parseAction(attributes + .getAttribute(Command.COMMAND_ATTRIBUTE_EXECUTE)); + command.setExecuteAction(action); + } + } else if (level_ == ActionsActionLevel) { + } + + if (formParser_ != null) { + formParser_.handleStartElement(element, ns, attributes); + } + } + + public void handleEndElement(String element, String ns) { + if (formParser_ != null) { + formParser_.handleEndElement(element, ns); + } + + Command command = getPayloadInternal(); + + if (level_ == FormOrNoteOrActionsLevel) { + if (formParser_ != null) { + Form form = (Form) (formParser_.getPayload()); + assert (form != null); + command.setForm(form); + formParser_ = null; + } else if (inNote_) { + inNote_ = false; + command.addNote(new Note(currentText_, noteType_)); + } else if (inActions_) { + inActions_ = false; + } + } else if ((level_ == ActionsActionLevel) && inActions_) { + Action action = parseAction(element); + command.addAvailableAction(action); + } + + --level_; + } + + public void handleCharacterData(String data) { + if (formParser_ != null) { + formParser_.handleCharacterData(data); + } else { + currentText_ += data; + } + } + + private static Action parseAction(String action) { + return Action.getAction(action); + } + + @Override + public String toString() { + return CommandParser.class.getSimpleName() + "\nlevel: " + level_ + + "\ncurrent text: " + currentText_ + "\nnote: " + inNote_ + + "\nactions: " + inActions_ + "\nnote type: " + noteType_; + } +} diff --git a/src/com/isode/stroke/parser/payloadparsers/FormParser.java b/src/com/isode/stroke/parser/payloadparsers/FormParser.java new file mode 100644 index 0000000..2ca50c1 --- /dev/null +++ b/src/com/isode/stroke/parser/payloadparsers/FormParser.java @@ -0,0 +1,349 @@ +/* + * 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 java.util.List; + +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.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.parser.AttributeMap; +import com.isode.stroke.parser.GenericPayloadParser; + +/** + * Parser for {@link Form} element. + */ +public class FormParser extends GenericPayloadParser<Form> { + private static abstract class FieldParseHelper<T> { + protected GenericFormField<T> field; + + public void addValue(String s) { + field.addRawValue(s); + } + + public GenericFormField<T> getField() { + return field; + } + } + + private static class BoolFieldParseHelper extends FieldParseHelper<Boolean> { + public void addValue(String s) { + field.setValue(((s.equals("1")) || (s.equals("true")))); + super.addValue(s); + } + } + + private static class StringFieldParseHelper extends + FieldParseHelper<String> { + 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<JID> { + public void addValue(String s) { + field.setValue(new JID(s)); + super.addValue(s); + } + }; + + private static class StringListFieldParseHelper extends + FieldParseHelper<List<String>> { + public void addValue(String s) { + List<String> l = field.getValue(); + l.add(s); + field.setValue(l); + super.addValue(s); + } + }; + + private static class JIDListFieldParseHelper extends + FieldParseHelper<List<JID>> { + public void addValue(String s) { + List<JID> 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<CommandParser>("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<Command> { + /** + * 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 += "</" + Command.COMMAND_ELEMENT_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<Form> { + /** + * 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<JID> 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<String> 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<String> 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<String, String> attributes_ = new HashMap<String, String>(); + private final Map<String, String> attributes_ = new TreeMap<String, String>(); private final Vector<XMLNode> childNodes_ = new Vector<XMLNode>(); 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); + } } |