Announcement

Collapse
No announcement yet.
X
  • Filter
  • Time
Clear All
new posts

    Send object or map via RPCResponse data? (SmartGWT Pro)

    Hi,

    I'm trying to send a non-trivial object back from an RPCRequestServlet. I can pass back a string (and cast it to a String on the client side), but can't seem to send a simple class or a map. I really just want to pass back data in a more organized way than munging it all together in a string.

    I've tried an object, Serializable object, a LinkedHashMap, but with no luck. I can pass a LinkedHashMap from the client to the server in a RPCRequest, and it gets nicely casted to a LinkedMap. But I'm not sure how to go the other way.

    The server docs indicate that I should be able to pass back pretty much any old object, as long as it's fairly primitive types, arrays, a map, etc.

    Can anyone share an example of what exact type they instantiate and pass as the "data" in the RPCResponse on the server, and then how they re-constitute it into a usable object on the server side (i.e. exact type they cast it to on the server)?

    Thanks much!

    #2
    Ok, so I found a way to send a LinkedHashMap in the RPCResponse from the server to the client, and to re-constitute it back into a usable Map on the client. It uses the JSOHelper to convert the JavaScriptObject back to a usable Java object:

    // Client code that initiates the RPC request and hears the response:
    Code:
    RPCManager.sendRequest(rpcRequest, new RPCCallback() {
    	@Override
    	public void execute(RPCResponse response, Object rawData, RPCRequest request) {
    		RPCActionResponse rpcActionResponse = null;
    		try {
    			JavaScriptObject jso = (JavaScriptObject)rawData;
    			Map<String,String> map = (Map<String,String>)JSOHelper.convertToMap(jso);
    
    			// Do something with our Java Map!
    			String msg = map.get("MyMessage");
    			// msg should now contain "Hello, world!"
    		}
    		catch(Exception ex) {
    			// Log this...didn't get our map?!?
    		}
    	}
    });
    // Server (servlet) code that sends the response back, raw data containing a map:
    Code:
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
    	RPCManager rpc = new RPCManager(request, response);
    	LinkedHashMap<String,String> map = new LinkedHashMap<String,String>();
    	map.put("MyMessage", "Hello, world!");
    	rpc.send(map);
    }
    I think this is pretty much what I have working.

    Is there a "best practice" for serializing an object and sending it? I.e. a bean?

    Comment


      #3
      Generally data is translated to "typeless" data such as Maps or Records. Only the field values retain their (atomic) types such as Date. This is the best practice for the vast majority of use cases, so that you avoid having to define redundant DTO classes to avoidw serialization issues with your data.

      If you're interested in re-creating a specific bean class on the client, please explain the overall use case and we can suggest the best approach for re-creating a bean in that scenario, or a better alternative approach.

      Comment


        #4
        Thanks for the info. Here's what's I'm up to...

        It's sometimes a regarded principle to have stronger type safety by using well-defined objects rather than putting everything into a somewhat un-typed array. I.e. the SmartGWT code base nicely makes specific strongly-typed enum values, instead of just making everything an int.

        For stronger type safety, I was initially thinking I'd make a "Response" object for responding to an RPC call (i.e. something I attempted to process failed, so here's the "false" return status, plus here's the "reason" why it failed, as well as some further "info" related to the reason). For example, {status:false, reason:"exception", info:"NullPointerException"}. This can be done as a map (which is what I've implemented for now), but was wondering if it can be an actual object.

        I'm also now using the Real-Time Messaging Module. If I notice on the server side that a business entity has changed (i.e. a "Bicycle" record in my DB has changed state), then it'd be nice to load the "Bicycle" record from the DB *once* on the server and to tell anyone who's listening "Hey, this Bicycle has changed, and here is the new record"...Instead of having each individual client then hit the database separately to load its own copy of the Record.

        Do you have a suggestion for how to handle broadcasting changed objects to clients?

        A further description of my scenario is that I have an "audit" table that gets updated whenever any row in the DB changes (whether it's a change made through the SmartServer or even a manual/SQL change to the DB). I'd like to setup a back-end thread to monitor this table and relevant changes to listening clients ("Hey, this Bicycle has changed!").

        Thoughts for how to best accomplish this with SmartServer/SmartGWT?

        Thank you!

        Comment


          #5
          On broadcasting changes, see the DataSource.updateCaches() API. As far as type safety, it's an intentional choice to have only field-level type safety - see explanation above.

          Comment


            #6
            I tried using DataSource.updateCaches() before, but it doesn't do enough for me. I.e. if I use the DataSource to "fetch" the updated record from the database (because I heard via RTMM that the record was changed by someone else), then I get a response back which has a DSRequest/DSResponse. However, that DSResponse is for a "fetch", and not an "add" or "update". So, when I say DS.updateCaches() with that DSResponse, the observing ListGrid doesn't get notified of the change.

            My understanding is that the caches would get updated only if I had done an "add" or "update" (I think I read this in the docs somewhere). If I do the fetch and then immediately do a DS.update(recordIjustFetched), then the observing ListGrid does get notified of the change. I don't think that doing a "update" on a record that I just read from the DB is a good idea, because the record could have been changed by someone else between the time I fetched and when I update.

            How do you suggest handling this?

            Thank you!

            Comment


              #7
              Generally, send the data for the updated record via messaging and form a DSResponse from that data client side. Don't do a separate fetch.

              Comment


                #8
                Ok, I see. The following is to hopefully help anyone else reading along...

                As a intermediate step for now, the following seems to work: Once I receive the DSResponse/DSRequest in the "fetch" DSCallback, I can then set the DSRequest.setOperationType() from "fetch" to be "update" and just pass the response/request to dataSource.updateCaches(dsResponse, dsRequest). Doing this eliminates me doing an extra (and possibly error-causing) datasource.update() to accomplish the same goal (of having databound components observe the new record).

                I'll look into sending the updated record from the server-side next.

                Thanks very much!

                Comment


                  #9
                  Not really following that.. seems as though you are overcomplicating things. The typical pattern is that when you receive data in your Messaging subscriber, you create a DSResponse (new DSResponse()) using the data you received via Messaging and pass that DSResponse to updateCaches().

                  If, for whatever reason, you are not able to send the changed data via Messaging, but you able to get an ID for the updated record, then yes you can do a separate DataSource.fetchData() call to get the complete record. However, re-using the DSResponse you receive in the callback from fetchData() is an unnecessary hack. Just create a new one (new DSResponse()) and populate it.

                  Comment


                    #10
                    Sorry, didn't mean to confuse the issue.

                    I was just saying that as a half-way step from where I started (notify -> fetch -> update), that I was able to get rid of the update to be (notify -> fetch -> updateCaches).

                    I agree that it'll be better to pass the details of the record as part of the notify, but I hadn't gotten that far yet. I'm working on that part next.

                    Comment


                      #11
                      Ah, got it. Now that makes sense :)

                      Comment


                        #12
                        One thing that isn't clear to me is how the DMI samples work. I haven't actually gotten the sample code to run on my local computer. I have my own DMI app that I think I'm doing the same thing the DMI samples do. It returns a List of bean objects, and the DevConsole shows the correct number of items in the list, but each list item is an empty map.

                        Specifically, does the bean actually get serialized and passed back across the wire? If so, what are the requirements to make that happen?

                        I.e. the samples imply that following would be serialized and returned by via a DMI-fetch's response:

                        Code:
                        public class MyBean implements Serializable {
                        	protected String name;
                        
                        	public void setName(String name) {
                        		this.name = name;
                        	}
                        
                        	public String getName() {
                        		return name;
                        	}
                        }
                        Thanks!

                        Comment


                          #13
                          Java Bean properties discovered by reflection will be sent to the client. There is no need for the object to be Serializable, in fact, part of the point is elimination of DTOs.

                          See also DataSource.dropExtraFields, on by default for DMI - most likely if you are seeing empty maps, your field names do not match the getter/setter names.

                          Comment


                            #14
                            For the serialization of the records to occur upon returning a response, does the DataSource XML doc need to define the serverType as "generic", or can I define it as "sql"?

                            Thanks.

                            Comment


                              #15
                              Ok, I figured it out. I'll give a little more context so others can follow.

                              My example is for searching "project" records:

                              ProjectRecord.java
                              Code:
                              public class ProjectRecord  {
                              	protected Long projectID_;  // member name doesn't matter here!
                              	protected String name_;
                              
                              	public ProjectRecord() {}
                              
                              	public void setProjectID(Long projectID) { projectID_ = projectID; }
                              	public void setName(String name) { name_ = name; }
                              
                              	public Long getProjectID() { return projectID_; }
                              	public String getName() { return name_; }
                              }
                              ProjectDMI.ds.xml:
                              Code:
                              <DataSource
                              	ID="ProjectDMI"
                              	serverType="sql"
                              	clientOnly="false"
                              >
                              	<fields>
                              		<field name="projectID" type="sequence" hidden="true" primaryKey="true" />
                              		<field name="name" type="text" required="true" length="50" />
                              	</fields>
                              
                              	<operationBindings>
                              		<operationBinding operationType="fetch" serverMethod="fetch" operationId="search" >
                              			<serverObject lookupStyle="new" className="com.foobar.testapp.server.dmi.SearchProjectsDMI" />
                              		</operationBinding>
                                	</operationBindings>
                              </DataSource>
                              SearchProjectsDMI.java:
                              Code:
                              public class SearchProjectsDMI {
                              	public DSResponse fetch(DSRequest dsRequest) throws Exception
                              	{
                              		List projectRecords = new ArrayList();
                              		for(int ii = 1; ii <= 5; ++ii) {
                              			ProjectRecord rec = new ProjectRecord();
                              			rec.setProjectID(ii);
                              			rec.setName("Proj-"+ii);
                              			projectRecords.add(rec);
                              		}
                              
                              		DSResponse dsResponse = new DSResponse();
                              		dsResponse.setDataSource(dsRequest.getDataSource());
                              		dsResponse.setStartRow(0);
                              		dsResponse.setEndRow(projectRecords.size());
                              		dsResponse.setTotalRows(projectRecords.size());
                              		dsResponse.setData(projectRecords);
                              
                              		return dsResponse;
                              	}
                              }
                              So, what was I missing? Well, I had the "name" of each field in the ProjectDMI.ds.xml as uppercase (i.e. "ProjectID" and "Name"), whereas they *MUST* be lowercase (i.e. "projectID" and "name").

                              To be clear:
                              1. The field names in the XML doc must begin with lowercase.
                              2. The corresponding getter and setter in the bean must be of the form "getProjectID()" and "setProjectID()".
                              3. (Bonus): the protected/private data members in the bean don't matter...they can be uppercase, lowercase, abbreviations, or even represented as a map...for the bean, it's the getters/setters only (I haven't tested this point exhaustively, but it's my strong belief).

                              One more interesting thing: when I set the dropExtraFields="false" in the DataSource, it will serialize the fields found in the bean, even when the Datasource field names start with uppercase letters. However, the serialized map representation has all names starting with lowercase (i.e. it appears as "projectID" and "name"). To me, this means that the serializer is just using all of the getters and creating corresponding entries in the map that start with the lowercase letter of the getter's field name.

                              Hope this helps someone!

                              Comment

                              Working...
                              X