/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2010 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package com.funambol.syncml.spds;
import java.util.Date;
import java.util.Vector;
import java.util.Hashtable;
import com.funambol.sync.SyncException;
import com.funambol.storage.StringKeyValueMemoryStore;
import com.funambol.storage.StringKeyValueStore;
import com.funambol.storage.StringKeyValueStoreFactory;
import com.funambol.sync.SyncException;
import com.funambol.sync.SyncItem;
import com.funambol.sync.SyncConfig;
import com.funambol.sync.SourceConfig;
import com.funambol.sync.ResumableSource;
import com.funambol.sync.client.BaseSyncSource;
import com.funambol.syncml.protocol.SyncML;
import com.funambol.syncml.protocol.Sync;
import com.funambol.syncml.protocol.SyncBody;
import com.funambol.syncml.protocol.Alert;
import com.funambol.syncml.protocol.Map;
import com.funambol.syncml.protocol.Cred;
import com.funambol.syncml.protocol.Meta;
import com.funambol.syncml.protocol.Target;
import com.funambol.util.CodedException;
import com.funambol.util.Log;
import com.funambol.util.ConsoleAppender;
import com.funambol.util.TransportAgent;
import junit.framework.*;
/**
* Test the SyncManager suspend and resume functionality
*/
public class SyncManagerSuspendResumeTest extends TestCase {
private static final String TAG_LOG = "SyncManagerSuspendResumeTest";
private final String TEST_SERVER_URL = "http://test.server.url";
private final String TEST_USERNAME = "test";
private final String TEST_PASSWORD = "test";
private SyncManager sm = null;
private SyncConfig sc = null;
private DeviceConfig dc = null;
private SourceConfig ssc = null;
private SuspendResumeSource tss = null;
private final String JSESSION_ID = ";jsessionid=F2EA56F802D65950FAC3E37336BE1EEA.NODE01";
private int sentMessagesCount = 0;
private static final int NUM_ITERATIONS = 80;
private final String TEST_END_MESSAGE = "Test Ended";
private StringKeyValueStore store = null;
private TestStringKeyValueStoreFactory storeFactory = new TestStringKeyValueStoreFactory();
public SyncManagerSuspendResumeTest(String name) {
super(name);
}
/**
* Set up all of the tests
*/
public void setUp() {
sc = new SyncConfig();
sc.syncUrl = TEST_SERVER_URL;
sc.userName = TEST_USERNAME;
sc.password = TEST_PASSWORD;
dc = new DeviceConfig();
ssc = new SourceConfig("briefcase", SourceConfig.BRIEFCASE_TYPE, "briefcase");
SyncMLAnchor anchor = new SyncMLAnchor();
ssc.setSyncAnchor(anchor);
sm = new SyncManager(sc, dc);
tss = new SuspendResumeSource(ssc);
sentMessagesCount = 0;
SyncStatus.setStoreFactory(storeFactory);
Log.initLog(new ConsoleAppender(), Log.TRACE);
}
/**
* Tear down all of the tests
*/
public void tearDown() {
}
//----------------------------------------------- Authentication phase tests
public void testSuspendSlow1() throws Exception {
sm.setTransportAgent(new TestTransportAgent(new TestMessageHandler() {
public byte[] handleMessage(byte message[]) throws Exception {
byte response[] = null;
sentMessagesCount++;
if (sentMessagesCount == 1) {
// Handle first message (Force a slow sync)
response = getInitResponse(false, 201).getBytes("UTF-8");
} else if (sentMessagesCount == 2) {
// The client is sending its items
int firstLuid = tss.getFirstLuid();
int lastLuid = tss.getLastLuid();
response = getRecevingResponse(firstLuid, lastLuid);
if (sentMessagesCount == NUM_ITERATIONS) {
// Block the source from providing items
tss.endSending();
}
sm.cancel();
} else if (sentMessagesCount == 3) {
// This is the resume request
SyncMLParser parser = new SyncMLParser(false);
SyncML syncML = parser.parse(message);
// We expect an alert with the resume request
SyncBody syncBody = syncML.getSyncBody();
Vector bodyCommands = syncBody.getCommands();
Alert alert = null;
for(int i=0;i<bodyCommands.size();++i) {
if (bodyCommands.elementAt(i) instanceof Alert) {
alert = (Alert)bodyCommands.elementAt(i);
break;
}
}
assertTrue(alert != null);
assertTrue(alert.getData() == SyncML.ALERT_CODE_RESUME);
throw new SyncException(SyncException.CLIENT_ERROR, TEST_END_MESSAGE);
}
// Note that we don't send the status to the map command, but
// currenly the SyncManager does not really care ;)
tss.resetLuidCounters();
return response;
}
public String handleMessage(String message) throws Exception {
return null;
}
}));
try {
sm.sync(tss);
} catch (SyncException se) {
// The sync must have been cancelled
assertTrue(se.getCode() == SyncException.CANCELLED);
}
try {
// Now we fire the same sync again and we expect it to be resumed
sm.sync(tss);
} catch (SyncException se) {
assertTrue(se.getCode() == SyncException.CLIENT_ERROR);
assertTrue(se.getMessage().endsWith(TEST_END_MESSAGE));
}
}
public void testSuspendFast1() throws Exception {
sm.setTransportAgent(new TestTransportAgent(new TestMessageHandler() {
public byte[] handleMessage(byte message[]) throws Exception {
byte response[] = null;
sentMessagesCount++;
if (sentMessagesCount == 1) {
response = getInitResponse(true, 200).getBytes("UTF-8");
tss.endSending();
} else if (sentMessagesCount == 2) {
// The client has no items to send
response = getSendingResponse(false);
} else if (sentMessagesCount == 3) {
// This message shall contain the client mappings, but the
// sync gets interrupted here
throw new SyncException(SyncException.CONN_NOT_FOUND, "IOError");
} else if (sentMessagesCount == 4) {
// This is the resume request
SyncMLParser parser = new SyncMLParser(false);
SyncML syncML = parser.parse(message);
// We expect an alert with the resume request
SyncBody syncBody = syncML.getSyncBody();
Vector bodyCommands = syncBody.getCommands();
Alert alert = null;
for(int i=0;i<bodyCommands.size();++i) {
if (bodyCommands.elementAt(i) instanceof Alert) {
alert = (Alert)bodyCommands.elementAt(i);
break;
}
}
assertTrue(alert != null);
// We don't force a resume here because our server will
// revert to a slow sync instead
//assertTrue(alert.getData() == SyncML.ALERT_CODE_RESUME);
response = getInitResponse(true, 200).getBytes("UTF-8");
//throw new SyncException(SyncException.CLIENT_ERROR, TEST_END_MESSAGE);
} else if (sentMessagesCount == 5) {
// We expect a message with 20 map items
SyncMLParser parser = new SyncMLParser(false);
SyncML syncML = parser.parse(message);
// We expect a map command with 20 map items
SyncBody syncBody = syncML.getSyncBody();
Vector bodyCommands = syncBody.getCommands();
Map map = null;
for(int i=0;i<bodyCommands.size();++i) {
if (bodyCommands.elementAt(i) instanceof Map) {
map = (Map)bodyCommands.elementAt(i);
break;
}
}
assertTrue(map != null);
Vector mapItems = map.getMapItems();
assertTrue(mapItems != null);
assertTrue(mapItems.size() == 20);
throw new SyncException(SyncException.CLIENT_ERROR, TEST_END_MESSAGE);
}
// Note that we don't send the status to the map command, but
// currenly the SyncManager does not really care ;)
tss.resetLuidCounters();
return response;
}
public String handleMessage(String message) throws Exception {
return null;
}
}));
try {
sm.sync(tss);
} catch (SyncException se) {
// The sync must have been cancelled
assertTrue(se.getCode() == SyncException.CLIENT_ERROR);
assertTrue(se.getMessage().endsWith("IOError"));
}
try {
// Now we fire the same sync again and we expect it to be resumed
sm.sync(tss);
} catch (SyncException se) {
assertTrue(se.getCode() == SyncException.CLIENT_ERROR);
assertTrue(se.getMessage().endsWith(TEST_END_MESSAGE));
}
}
private String getInitResponse(boolean acceptSyncMode, int forcedSyncMode) {
return getServerResponseFromStatus(getStatus_AUTH_BASIC_OK(), !acceptSyncMode, forcedSyncMode);
}
private String getStatus_AUTH_BASIC_OK() {
return "<Status>\n" +
"<CmdID>1</CmdID>\n" +
"<MsgRef>1</MsgRef>\n" +
"<CmdRef>0</CmdRef>\n" +
"<Cmd>SyncHdr</Cmd>\n" +
"<TargetRef>" + sc.syncUrl + "</TargetRef>\n" +
"<SourceRef>" + dc.getDevID() + "</SourceRef>\n" +
"<Data>212</Data>\n" +
"</Status>";
}
private String getServerResponseFromStatus(String status, boolean addAlertFromServer, int forcedSyncMode) {
return "<SyncML>\n" +
"<SyncHdr>\n" +
"<VerDTD>1.2</VerDTD>\n" +
"<VerProto>SyncML/1.2</VerProto>\n" +
"<SessionID>1266917419910</SessionID>\n" +
"<MsgID>1</MsgID>\n" +
"<Target>\n" +
"<LocURI>" + dc.getDevID() + "</LocURI>\n" +
"</Target>\n" +
"<Source>\n" +
"<LocURI>" + sc.syncUrl + "</LocURI>\n" +
"</Source>\n" +
"<RespURI>" + sc.syncUrl + JSESSION_ID + "</RespURI>\n" +
"</SyncHdr>\n" +
"<SyncBody>\n" +
status +
"<Status>\n" +
"<CmdID>2</CmdID>\n" +
"<MsgRef>1</MsgRef>\n" +
"<CmdRef>1</CmdRef>\n" +
"<Cmd>Alert</Cmd>\n" +
"<TargetRef>" + ssc.getName() + "</TargetRef>\n" +
"<SourceRef>" + ssc.getRemoteUri() + "</SourceRef>\n" +
"<Data>" + (addAlertFromServer ? 508 : 200) + "</Data>\n" +
"</Status>\n" +
"<Status>\n" +
"<CmdID>3</CmdID>\n" +
"<MsgRef>1</MsgRef>\n" +
"<CmdRef>2</CmdRef>\n" +
"<Cmd>Put</Cmd>\n" +
"<SourceRef>./devinf12</SourceRef>\n" +
"<Data>200</Data>\n" +
"</Status>\n" +
getAlertFromServer(forcedSyncMode) +
"<Final/>\n" +
"</SyncBody>\n" +
"</SyncML>";
}
private String getAlertFromServer(int forcedSyncMode) {
return "<Alert>\n" +
"<CmdID>4</CmdID>\n" +
"<Data>" + forcedSyncMode + "</Data>\n" +
"<Item>\n" +
"<Target>\n" +
"<LocURI>" + ssc.getName() + "</LocURI>\n" +
"</Target>\n" +
"<Source>\n" +
"<LocURI>" + ssc.getRemoteUri() + "</LocURI>\n" +
"</Source>\n" +
"<Meta>\n" +
"<Anchor xmlns='syncml:metinf'>\n" +
"<Last>0</Last>\n" +
"<Next>0</Next>\n" +
"</Anchor>\n" +
"</Meta>\n" +
"</Item>\n" +
"</Alert>\n";
}
private byte[] getRecevingResponse(int firstLuid, int lastLuid) {
StringBuffer res = new StringBuffer();
res.append("<SyncML>\n")
.append("<SyncHdr>\n")
.append("<VerDTD>1.2</VerDTD>\n")
.append("<VerProto>SyncML/1.2</VerProto>\n")
.append("<SessionID>1266917419910</SessionID>\n")
.append("<MsgID>1</MsgID>\n")
.append("<Target>\n")
.append("<LocURI>").append(dc.getDevID()).append("</LocURI>\n")
.append("</Target>\n")
.append("<Source>\n")
.append("<LocURI>" + sc.syncUrl + "</LocURI>\n")
.append("</Source>\n")
.append("<RespURI>").append(sc.syncUrl).append(JSESSION_ID).append("</RespURI>\n")
.append("</SyncHdr>\n")
.append("<SyncBody>\n");
int cmdId = 1;
res.append("<Status>\n")
.append("<CmdID>").append(cmdId++).append("</CmdID>\n")
.append("<MsgRef>1</MsgRef>\n")
.append("<CmdRef>0</CmdRef>\n")
.append("<Cmd>SyncHdr</Cmd>\n")
.append("<TargetRef>").append(sc.syncUrl).append("</TargetRef>\n")
.append("<SourceRef>").append(dc.getDevID()).append("</SourceRef>\n")
.append("<Data>200</Data>\n")
.append("</Status>");
for(int i=firstLuid;i<lastLuid+1;++i) {
res.append("<Status>\n")
.append("<CmdID>").append(cmdId++).append("</CmdID>\n")
.append("<MsgRef>1</MsgRef>\n")
.append("<CmdRef>0</CmdRef>\n")
.append("<Cmd>Replace</Cmd>\n")
.append("<SourceRef>").append(i).append("</SourceRef>\n")
.append("<Data>200</Data>\n")
.append("</Status>");
}
res.append("</SyncBody>\n")
.append("</SyncML>");
return res.toString().getBytes();
}
private byte[] getSendingResponse(boolean last) {
StringBuffer res = new StringBuffer();
res.append("<SyncML>\n")
.append("<SyncHdr>\n")
.append("<VerDTD>1.2</VerDTD>\n")
.append("<VerProto>SyncML/1.2</VerProto>\n")
.append("<SessionID>1266917419910</SessionID>\n")
.append("<MsgID>1</MsgID>\n")
.append("<Target>\n")
.append("<LocURI>").append(dc.getDevID()).append("</LocURI>\n")
.append("</Target>\n")
.append("<Source>\n")
.append("<LocURI>" + sc.syncUrl + "</LocURI>\n")
.append("</Source>\n")
.append("<RespURI>").append(sc.syncUrl).append(JSESSION_ID).append("</RespURI>\n")
.append("</SyncHdr>\n")
.append("<SyncBody>\n");
int cmdId = 1;
res.append("<Status>\n")
.append("<CmdID>").append(cmdId++).append("</CmdID>\n")
.append("<MsgRef>1</MsgRef>\n")
.append("<CmdRef>0</CmdRef>\n")
.append("<Cmd>SyncHdr</Cmd>\n")
.append("<TargetRef>").append(sc.syncUrl).append("</TargetRef>\n")
.append("<SourceRef>").append(dc.getDevID()).append("</SourceRef>\n")
.append("<Data>200</Data>\n")
.append("</Status>");
res.append("<Sync>\n")
.append("<CmdID>").append(cmdId++).append("</CmdID>\n")
.append("<Target>\n")
.append("<LocURI>briefcase</LocURI>\n")
.append("</Target>\n")
.append("<Source>\n")
.append("<LocURI>briefcase</LocURI>\n")
.append("</Source>\n");
for(int i=0;i<20;++i) {
res.append("<Add>\n")
.append("<CmdID>").append(cmdId++).append("</CmdID>\n")
.append("<Item>\n")
.append("<Source>\n")
.append("<LocURI>1205").append(i).append("</LocURI>\n")
.append("</Source>\n")
.append("<Meta>\n")
.append("<Type xmlns=\"syncml:metinf\">application/*</Type>\n")
.append("</Meta>\n")
.append("<Data>This is just a test for performance evaluation</Data>\n")
.append("</Item>\n")
.append("</Add>\n");
}
res.append("</Sync>\n");
if (last) {
res.append("<Final/>\n");
}
res.append("</SyncBody>\n")
.append("</SyncML>");
return res.toString().getBytes();
}
private class TestTransportAgent implements TransportAgent {
private TestMessageHandler handler;
public TestTransportAgent(TestMessageHandler h) {
handler = h;
}
public String sendMessage(String request, String charset) throws CodedException {
try {
return handler.handleMessage(request);
} catch (Exception e) {
throw new CodedException(-1, e.toString());
}
}
public String sendMessage(String request) throws CodedException {
return sendMessage(request, null);
}
public byte[] sendMessage(byte[] request) throws CodedException {
try {
return handler.handleMessage(request);
} catch (Exception e) {
throw new CodedException(-1, e.toString());
}
}
public void setRetryOnWrite(int retries) { }
public void setRequestURL(String requestUrl) { }
public String getResponseDate() { return new Date().toString(); }
public void setRequestContentType(String contentType) {
}
public void setCustomHeaders(Hashtable headers) {
}
}
private interface TestMessageHandler {
public String handleMessage(String message) throws Exception;
public byte[] handleMessage(byte[] message) throws Exception;
}
private class TestSyncSource extends BaseSyncSource {
private boolean done = false;
private int luid = 0;
private int firstLuid = -1;
private int lastLuid = -1;
public TestSyncSource(SourceConfig sc) {
super(sc);
}
public int addItem(SyncItem item) throws SyncException {
return 200;
}
public int updateItem(SyncItem item) throws SyncException {
return 200;
}
public int deleteItem(String key) throws SyncException {
return 200;
}
public SyncItem getNextItem() throws SyncException {
if (firstLuid == -1) {
firstLuid = luid;
}
lastLuid = luid;
if (!done) {
SyncItem res = new SyncItem("" + luid);
res.setContent("Test item, make is of a signficant length".getBytes());
luid++;
return res;
} else {
return null;
}
}
public void endSending() {
done = true;
}
public void resetLuidCounters() {
firstLuid = -1;
lastLuid = -1;
}
public int getFirstLuid() {
return firstLuid;
}
public int getLastLuid() {
return lastLuid;
}
protected void initAllItems() throws SyncException {
}
protected void initNewItems() throws SyncException {
}
protected void initUpdItems() throws SyncException {
}
protected void initDelItems() throws SyncException {
}
protected SyncItem getItemContent(final SyncItem item) throws SyncException {
return null;
}
public void cancel() {
}
}
private class TestStringKeyValueStoreFactory extends StringKeyValueStoreFactory {
private StringKeyValueMemoryStore store = new StringKeyValueMemoryStore();
public TestStringKeyValueStoreFactory() {
}
public StringKeyValueStore getStringKeyValueStore(String name) {
return store;
}
}
private class SuspendResumeSource extends TestSyncSource implements ResumableSource {
public SuspendResumeSource(SourceConfig config) {
super(config);
}
public boolean readyToResume() {
return true;
}
public String getLuid(SyncItem item) {
return null;
}
public long getPartiallyReceivedItemSize(String key) {
return -1;
}
}
}