/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* 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 Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xforms.state;
import org.junit.Test;
import org.orbeon.oxf.externalcontext.ExternalContext;
import org.orbeon.oxf.externalcontext.TestSession;
import org.orbeon.oxf.test.ResourceManagerTestBase;
import org.orbeon.oxf.util.NetUtils;
import org.orbeon.oxf.util.SecureUtils;
import org.orbeon.oxf.util.StringUtils;
import org.orbeon.oxf.xforms.XFormsContainingDocument;
import org.orbeon.oxf.xforms.XFormsProperties;
import org.orbeon.oxf.xforms.XFormsStaticState;
import org.orbeon.oxf.xforms.analysis.XFormsStaticStateTest;
import org.orbeon.oxf.xforms.event.ClientEvents;
import org.orbeon.oxf.xforms.event.XFormsEvent;
import org.orbeon.oxf.xforms.event.XFormsEventTarget;
import org.orbeon.oxf.xforms.event.events.XXFormsValueEvent;
import scala.Option;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import static org.junit.Assert.*;
public class XFormsStateManagerTest extends ResourceManagerTestBase {
private XFormsStateLifecycle stateManager = XFormsStateManager.instance();
@Test
public void testClientNoCache() {
testClient(false, "client-nocache.xhtml");
}
@Test
public void testClientDoCache() {
testClient(true, "client-cache.xhtml");
}
@Test
public void testServerNoCache() {
testServer(false, "server-nocache.xhtml");
}
@Test
public void testServerDoCache() {
testServer(true, "server-cache.xhtml");
}
@Test
public void testDocumentCacheSessionListener() {
// Create document
final ExternalContext.Session session = NetUtils.getSession(true);
final XFormsStaticState staticState = XFormsStaticStateTest.getStaticState("oxf:/org/orbeon/oxf/xforms/state/server-cache.xhtml");
final XFormsContainingDocument document = new XFormsContainingDocument(staticState, null, null, true);
stateManager.afterInitialResponse(document, null);
// Check there is a state manager session listener for this document
assertNotNull(session.javaGetAttribute(XFormsStateManager.getListenerSessionKey(document.getUUID())));
// Test that the document is in cache
assertSame(document, XFormsDocumentCache$.MODULE$.take(document.getUUID()).get());
// Expire session
((TestSession) session).expireSession();
// Test that the document is no longer in cache
assertTrue(XFormsDocumentCache$.MODULE$.take(document.getUUID()).isEmpty());
}
private static class State {
public XFormsContainingDocument document;
public String uuid;
public String staticStateString;
public String dynamicStateString;
}
private interface EventCallback {
List<XFormsEvent> createEvents(XFormsContainingDocument document);
}
private void testClient(boolean isCache, String formFile) {
final XFormsStaticState staticState = XFormsStaticStateTest.getStaticState("oxf:/org/orbeon/oxf/xforms/state/" + formFile);
// Initialize document and get initial document state
final State state1 = new State();
{
NetUtils.getSession(true); // make sure a session is in place as it is used by the state manager
state1.document = new XFormsContainingDocument(staticState, null, null, true);
state1.uuid = state1.document.getUUID();
state1.staticStateString = stateManager.getClientEncodedStaticState(state1.document);
state1.dynamicStateString = stateManager.getClientEncodedDynamicStateJava(state1.document);
assertEquals(state1.uuid.length(), SecureUtils.HexIdLength());
assertNotNull(StringUtils.trimAllToNull(state1.staticStateString));
assertNotNull(StringUtils.trimAllToNull(state1.dynamicStateString));
// Initial response sent
state1.document.afterInitialResponse();
stateManager.afterInitialResponse(state1.document, null);
}
assertEquals(1, getSequenceNumber(state1.dynamicStateString));
// Run update
final State state2 = doUpdate(isCache, state1, new EventCallback() {
public List<XFormsEvent> createEvents(XFormsContainingDocument document) {
final XFormsEventTarget input = (XFormsEventTarget) document.getObjectByEffectiveId("my-input");
return Collections.<XFormsEvent>singletonList(new XXFormsValueEvent(input, "gaga"));
}
});
// UUID and static state can't change
assertEquals(state1.uuid, state2.uuid);
assertEquals(state1.staticStateString, state2.staticStateString);
// Sequence must be updated
assertEquals(2, getSequenceNumber(state2.dynamicStateString));
// Dynamic state must have changed
assertFalse(stripSequenceNumber(state1.dynamicStateString).equals(stripSequenceNumber(state2.dynamicStateString)));
// Run update
final State state3 = doUpdate(isCache, state2, null);
// UUID and static state can't change
assertEquals(state1.uuid, state3.uuid);
assertEquals(state1.staticStateString, state3.staticStateString);
// Sequence must be updated
assertEquals(3, getSequenceNumber(state3.dynamicStateString));
// Dynamic state must NOT have changed because no event was dispatched
assertEquals(stripSequenceNumber(state2.dynamicStateString), stripSequenceNumber(state3.dynamicStateString));
// Get back to initial state
final State state4 = getInitialState(state1);
// UUID and static state can't change
assertEquals(state1.uuid, state4.uuid);
assertEquals(state1.staticStateString, state4.staticStateString);
// Sequence must be updated
assertEquals(1, getSequenceNumber(state4.dynamicStateString));
// Make sure we found the initial dynamic state
assertEquals(stripSequenceNumber(state1.dynamicStateString), stripSequenceNumber(state4.dynamicStateString));
}
private void testServer(boolean isCache, String formFile) {
final XFormsStaticState staticState = XFormsStaticStateTest.getStaticState("oxf:/org/orbeon/oxf/xforms/state/" + formFile);
// Initialize document and get initial document state
final String initialDynamicStateString;
final State state1 = new State();
{
NetUtils.getSession(true);
state1.document = new XFormsContainingDocument(staticState, null, null, true);
state1.uuid = state1.document.getUUID();
state1.staticStateString = stateManager.getClientEncodedStaticState(state1.document);
state1.dynamicStateString = stateManager.getClientEncodedDynamicStateJava(state1.document);
assertEquals(state1.uuid.length(), SecureUtils.HexIdLength());
assertNull(StringUtils.trimAllToNull(state1.staticStateString));
assertNull(StringUtils.trimAllToNull(state1.dynamicStateString));
// Initial response sent
state1.document.afterInitialResponse();
stateManager.afterInitialResponse(state1.document, null);
initialDynamicStateString = DynamicState.encodeDocumentToString(state1.document, XFormsProperties.isGZIPState(), false);
}
assertEquals(1, state1.document.getSequence());
// Run update
final State state2 = doUpdate(isCache, state1, new EventCallback() {
public List<XFormsEvent> createEvents(XFormsContainingDocument document) {
final XFormsEventTarget input = (XFormsEventTarget) document.getObjectByEffectiveId("my-input");
return Collections.<XFormsEvent>singletonList(new XXFormsValueEvent(input, "gaga"));
}
});
// UUID can't change
assertEquals(state1.uuid, state2.uuid);
// Sequence must be updated
assertEquals(2, state2.document.getSequence());
// State strings must be null
assertNull(StringUtils.trimAllToNull(state2.staticStateString));
assertNull(StringUtils.trimAllToNull(state2.dynamicStateString));
// Run update
final State state3 = doUpdate(isCache, state2, null);
// UUID can't change
assertEquals(state1.uuid, state3.uuid);
// Sequence must be updated
assertEquals(3, state3.document.getSequence());
// State strings must be null
assertNull(StringUtils.trimAllToNull(state3.staticStateString));
assertNull(StringUtils.trimAllToNull(state3.dynamicStateString));
// Get back to initial state
final State state4 = getInitialState(state1);
// UUID can't change
assertEquals(state1.uuid, state4.uuid);
// Sequence must be updated
assertEquals(1, state4.document.getSequence());
// State strings must be null
assertNull(StringUtils.trimAllToNull(state4.staticStateString));
assertNull(StringUtils.trimAllToNull(state4.dynamicStateString));
// Make sure we found the initial dynamic state
{
assertEquals(
stripSequenceNumber(initialDynamicStateString),
stripSequenceNumber(DynamicState.encodeDocumentToString(state4.document, XFormsProperties.isGZIPState(), false))
);
}
}
private State doUpdate(boolean isCache, final State state1, EventCallback callback) {
final RequestParameters parameters =
RequestParameters.apply(state1.uuid,
scala.Option.apply(null),
scala.Option.apply(state1.staticStateString),
scala.Option.apply(state1.dynamicStateString)
);
// New state
final State state2 = new State();
final Option<Lock> lock = stateManager.acquireDocumentLock(parameters.uuid(), 0L);
if (lock.isEmpty())
fail("Ajax update lock timeout exceeded");
try {
state2.document = stateManager.beforeUpdate(parameters);
if (isCache)
assertSame(state1.document, state2.document);// must be the same because cache is enabled and cache has room
else
assertNotSame(state1.document, state2.document);// can't be the same because cache is disabled
// Run events if any
state2.document.beforeExternalEvents(null);
if (callback != null) {
for (final XFormsEvent event: callback.createEvents(state2.document)) {
ClientEvents.processEvent(state2.document, event);
}
}
state2.document.afterExternalEvents();
stateManager.beforeUpdateResponse(state2.document, false);
// New state
state2.uuid = state2.document.getUUID();
state2.staticStateString = stateManager.getClientEncodedStaticState(state2.document);
state2.dynamicStateString = stateManager.getClientEncodedDynamicStateJava(state2.document);
stateManager.afterUpdateResponse(state2.document);
stateManager.afterUpdate(state2.document, true);
} finally {
stateManager.releaseDocumentLock(lock.get());
}
return state2;
}
private State getInitialState(final State state1) {
final RequestParameters parameters =
RequestParameters.apply(
state1.uuid,
scala.Option.apply(null),
scala.Option.apply(state1.staticStateString),
scala.Option.apply(state1.dynamicStateString)
);
// New state
final State state2 = new State();
// Find document
state2.document = XFormsStateManager.instance().findOrRestoreDocument(parameters, true, false);
assertNotSame(state1.document, state2.document);// can't be the same because either cache is disabled OR we create a duplicate document (could be same if state1 is initial state)
// New state
state2.uuid = state2.document.getUUID();
state2.staticStateString = stateManager.getClientEncodedStaticState(state2.document);
state2.dynamicStateString = stateManager.getClientEncodedDynamicStateJava(state2.document);
return state2;
}
private String stripSequenceNumber(String serializedState) {
final DynamicState dynamicState = DynamicState.apply(serializedState);
final DynamicState newDynamicState = dynamicState.copyUpdateSequence(1);
return newDynamicState.encodeToString(XFormsProperties.isGZIPState(), false);
}
private long getSequenceNumber(String serializedState) {
return DynamicState.apply(serializedState).sequence();
}
}