Fixes WAVE-417 Adds GadgetStateChange event generation for robot API.
diff --git a/build.properties b/build.properties
index 7f98247..86aad10 100644
--- a/build.properties
+++ b/build.properties
@@ -16,7 +16,7 @@
 # under the License.
 
 # Current versions
-waveinabox.version=0.4.0
+waveinabox.version=0.6.0
 
 # Names
 name=wave-in-a-box
diff --git a/src/org/waveprotocol/box/server/robots/passive/EventGenerator.java b/src/org/waveprotocol/box/server/robots/passive/EventGenerator.java
index 206a33b..b45a0f8 100644
--- a/src/org/waveprotocol/box/server/robots/passive/EventGenerator.java
+++ b/src/org/waveprotocol/box/server/robots/passive/EventGenerator.java
@@ -23,56 +23,43 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.wave.api.BlipData;
 import com.google.wave.api.Context;
+import com.google.wave.api.Gadget;
 import com.google.wave.api.data.converter.ContextResolver;
 import com.google.wave.api.data.converter.EventDataConverter;
-import com.google.wave.api.event.AnnotatedTextChangedEvent;
-import com.google.wave.api.event.DocumentChangedEvent;
-import com.google.wave.api.event.Event;
-import com.google.wave.api.event.EventType;
-import com.google.wave.api.event.FormButtonClickedEvent;
-import com.google.wave.api.event.WaveletBlipCreatedEvent;
-import com.google.wave.api.event.WaveletBlipRemovedEvent;
-import com.google.wave.api.event.WaveletParticipantsChangedEvent;
-import com.google.wave.api.event.WaveletSelfAddedEvent;
-import com.google.wave.api.event.WaveletSelfRemovedEvent;
+import com.google.wave.api.event.*;
 import com.google.wave.api.impl.EventMessageBundle;
 import com.google.wave.api.robot.Capability;
 import com.google.wave.api.robot.RobotName;
-
 import org.waveprotocol.box.server.robots.util.ConversationUtil;
 import org.waveprotocol.box.server.util.WaveletDataUtil;
-import org.waveprotocol.wave.model.conversation.Conversation;
-import org.waveprotocol.wave.model.conversation.ConversationBlip;
-import org.waveprotocol.wave.model.conversation.ConversationListenerImpl;
-import org.waveprotocol.wave.model.conversation.ObservableConversation;
-import org.waveprotocol.wave.model.conversation.ObservableConversationBlip;
-import org.waveprotocol.wave.model.conversation.WaveletBasedConversation;
-import org.waveprotocol.wave.model.document.DocHandler;
-import org.waveprotocol.wave.model.document.ObservableDocument;
+import org.waveprotocol.wave.model.conversation.*;
 import org.waveprotocol.wave.model.document.Doc.E;
 import org.waveprotocol.wave.model.document.Doc.N;
 import org.waveprotocol.wave.model.document.Doc.T;
+import org.waveprotocol.wave.model.document.DocHandler;
+import org.waveprotocol.wave.model.document.ObservableDocument;
 import org.waveprotocol.wave.model.document.indexed.DocumentEvent;
 import org.waveprotocol.wave.model.document.indexed.DocumentEvent.AnnotationChanged;
+import org.waveprotocol.wave.model.document.indexed.DocumentEvent.AttributesModified;
 import org.waveprotocol.wave.model.document.indexed.DocumentEvent.ContentInserted;
-import org.waveprotocol.wave.model.document.raw.impl.Element;
+import org.waveprotocol.wave.model.document.raw.impl.Node;
 import org.waveprotocol.wave.model.operation.OperationException;
 import org.waveprotocol.wave.model.operation.SilentOperationSink;
 import org.waveprotocol.wave.model.operation.wave.BasicWaveletOperationContextFactory;
 import org.waveprotocol.wave.model.operation.wave.TransformedWaveletDelta;
 import org.waveprotocol.wave.model.operation.wave.WaveletBlipOperation;
 import org.waveprotocol.wave.model.operation.wave.WaveletOperation;
-import org.waveprotocol.wave.model.wave.ObservableWavelet;
-import org.waveprotocol.wave.model.wave.ParticipantId;
-import org.waveprotocol.wave.model.wave.ParticipationHelper;
-import org.waveprotocol.wave.model.wave.WaveletListener;
+import org.waveprotocol.wave.model.wave.*;
 import org.waveprotocol.wave.model.wave.data.ObservableWaveletData;
 import org.waveprotocol.wave.model.wave.opbased.OpBasedWavelet;
 import org.waveprotocol.wave.model.wave.opbased.WaveletListenerImpl;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Generates Robot API Events from operations applied to a Wavelet.
@@ -87,7 +74,7 @@
  * <li>DocumentChanged (DONE)</li>
  * <li>AnnotatedTextChanged (DONE)</li>
  * <li>FormButtonClicked (TBD)</li>
- * <li>GadgetStateChanged (TBD)</li>
+ * <li>GadgetStateChanged (DONE)</li>
  * <li>BlipContributorChanged (TBD)</li>
  * <li>WaveletTagsChanged (TBD)</li>
  * <li>WaveletTitleChanged (TBD)</li>
@@ -258,13 +245,19 @@
      */
     private boolean documentChangedEventGenerated;
 
+    private EventDataConverter converter;
+    private Wavelet wavelet;
+
     public EventGeneratingDocumentHandler(ObservableDocument doc, ConversationBlip blip,
         Map<EventType, Capability> capabilities, EventMessageBundle messages,
-        ParticipantId deltaAuthor, Long deltaTimestamp) {
+        ParticipantId deltaAuthor, Long deltaTimestamp, Wavelet wavelet,
+        EventDataConverter converter) {
       this.doc = doc;
       this.blip = blip;
       this.capabilities = capabilities;
       this.messages = messages;
+      this.converter = converter;
+      this.wavelet = wavelet;
       setAuthorAndTimeStamp(deltaAuthor, deltaTimestamp);
     }
 
@@ -283,10 +276,64 @@
             addEvent(apiEvent, capabilities, blip.getId(), messages);
           }
         } else {
+          // used to distinguish between attribute changes and gadget state
+          // changes
+          Boolean gadgetStateChangeEvent = false;
+          if (eventComponent.getType() == DocumentEvent.Type.ATTRIBUTES) {
+            if (capabilities.containsKey(EventType.GADGET_STATE_CHANGED)) {
+              Map<String, String> oldState = new HashMap<>();
+              Integer index = -1;
+              try {
+                AttributesModified<N, E, T> attributesModified =
+                    (AttributesModified<N, E, T>) eventComponent;
+                // When a gadget state changes, the AttributesModifies event has
+                // always
+                // an oldValue map of the form {"value", something} (key is
+                // always value).
+                // To obtain the key of the changed state, the attribute "name"
+                // has to be obtained
+                // from the Element of the AttributesModified event.
+                String name =
+                    ((org.waveprotocol.wave.model.document.raw.impl.Element) attributesModified
+                        .getElement()).getAttribute("name");
+                String oldValue = attributesModified.getOldValues().get("value");
+                if (name != null || oldValue != null) {
+                  oldState.put(name, oldValue);
+                }
+                BlipData b = converter.toBlipData(blip, wavelet, messages);
+                Map<Integer, com.google.wave.api.Element> elements = b.getElements();
+                Set<Integer> keys = elements.keySet();
+                // The gadget element provided by the eventComponent
+                org.waveprotocol.wave.model.document.raw.impl.Element rawGadget =
+                    ((Node) attributesModified.getElement()).getParentElement();
+                for (Integer key : keys) {
+                  try {
+                    Gadget gadget = (Gadget) elements.get(key);
+                    if (sameGadgets(rawGadget, gadget)) {
+                      index = key;
+                      break;
+                    }
+                  } catch (ClassCastException e) {
+                    // if it is not a gadget we do not compare them
+                  }
+                }
+              } catch (ClassCastException e) {
+                e.printStackTrace();
+              }
+              if (oldState.size() != 0 && index != -1) {
+                // if the attribute changed belongs to a gadget
+                gadgetStateChangeEvent = true;
+                final GadgetStateChangedEvent gadgetEvent =
+                    new GadgetStateChangedEvent(null, messages, deltaAuthor.getAddress(),
+                        deltaTimestamp, blip.getId(), index, oldState);
+                addEvent(gadgetEvent, capabilities, blip.getId(), messages);
+              }
+            }
+          }
           if (capabilities.containsKey(EventType.FORM_BUTTON_CLICKED)) {
             if (eventComponent.getType() == DocumentEvent.Type.CONTENT_INSERTED) {
               ContentInserted<N, E, T> contentInserted = (ContentInserted<N, E, T>) eventComponent;
-              Element elementInserted = ((Element) contentInserted.getSubtreeElement());
+              org.waveprotocol.wave.model.document.raw.impl.Element elementInserted = ((org.waveprotocol.wave.model.document.raw.impl.Element) contentInserted.getSubtreeElement());
               if (elementInserted.getTagName().equals("click")) {
                 FormButtonClickedEvent buttonClickedEvent =
                     new FormButtonClickedEvent(null, null,
@@ -299,9 +346,10 @@
 
           } else
           if (capabilities.containsKey(EventType.DOCUMENT_CHANGED)
-              && !documentChangedEventGenerated) {
-            DocumentChangedEvent apiEvent = new DocumentChangedEvent(
-                null, null, deltaAuthor.getAddress(), deltaTimestamp, blip.getId());
+              && !documentChangedEventGenerated && !gadgetStateChangeEvent) {
+            DocumentChangedEvent apiEvent =
+                new DocumentChangedEvent(null, null, deltaAuthor.getAddress(), deltaTimestamp,
+                    blip.getId());
             addEvent(apiEvent, capabilities, blip.getId(), messages);
             // Only one documentChangedEvent should be generated per bundle.
             documentChangedEventGenerated = true;
@@ -324,6 +372,21 @@
       this.deltaAuthor = author;
       this.deltaTimestamp = timestamp;
     }
+
+    /**
+     * Check if an {@link org.waveprotocol.wave.model.document.raw.impl.Element}
+     * is and a {@link Gadget}
+     *
+     * @param rawElement
+     * @param element
+     * @return
+     */
+    private boolean sameGadgets(org.waveprotocol.wave.model.document.raw.impl.Element rawElement,
+        Gadget element) {
+      String ifr1 = rawElement.getAttribute("ifr");
+      String ifr2 = element.getProperty("ifr");
+      return (ifr1 != null && ifr1.equals(ifr2));
+    }
   }
 
   /**
@@ -473,7 +536,7 @@
           // Check if we need to attach a doc handler.
           if ((op instanceof WaveletBlipOperation)) {
             attachDocHandler(conversation, op, docHandlers, capabilities, messages,
-                delta.getAuthor(), timestamp);
+                delta.getAuthor(), timestamp, wavelet, converter);
           }
           op.apply(snapshot);
         }
@@ -515,7 +578,7 @@
   private void attachDocHandler(ObservableConversation conversation, WaveletOperation op,
       Map<String, EventGeneratingDocumentHandler> docHandlers,
       Map<EventType, Capability> capabilities, EventMessageBundle messages,
-      ParticipantId deltaAuthor, long timestamp) {
+      ParticipantId deltaAuthor, long timestamp, Wavelet wavelet, EventDataConverter converter) {
     WaveletBlipOperation blipOp = (WaveletBlipOperation) op;
     String blipId = blipOp.getBlipId();
     // Ignoring the documents outside the conversation such as tags
@@ -527,8 +590,9 @@
       EventGeneratingDocumentHandler docHandler = docHandlers.get(blipId1);
       if (docHandler == null) {
         ObservableDocument doc = (ObservableDocument) blip.getContent();
-        docHandler = new EventGeneratingDocumentHandler(
-            doc, blip, capabilities, messages, deltaAuthor, timestamp);
+        docHandler =
+            new EventGeneratingDocumentHandler(doc, blip, capabilities, messages, deltaAuthor,
+                timestamp, wavelet, converter);
         doc.addListener(docHandler);
         docHandlers.put(blipId1, docHandler);
       } else {