diff options
Diffstat (limited to '')
-rw-r--r-- | dbaccess/qa/complex/dbaccess/DatabaseDocument.java | 995 |
1 files changed, 995 insertions, 0 deletions
diff --git a/dbaccess/qa/complex/dbaccess/DatabaseDocument.java b/dbaccess/qa/complex/dbaccess/DatabaseDocument.java new file mode 100644 index 000000000..5bd28053b --- /dev/null +++ b/dbaccess/qa/complex/dbaccess/DatabaseDocument.java @@ -0,0 +1,995 @@ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +package complex.dbaccess; + +import com.sun.star.configuration.theDefaultProvider; +import com.sun.star.lang.NotInitializedException; +import com.sun.star.frame.DoubleInitializationException; +import com.sun.star.awt.XTopWindow; +import com.sun.star.beans.PropertyState; +import com.sun.star.document.DocumentEvent; +import com.sun.star.lang.XEventListener; +import com.sun.star.lang.XMultiServiceFactory; +import com.sun.star.script.XStorageBasedLibraryContainer; +import com.sun.star.task.XInteractionRequest; + +import com.sun.star.uno.Type; +import com.sun.star.uno.UnoRuntime; +import com.sun.star.frame.XStorable; +import com.sun.star.beans.NamedValue; +import com.sun.star.beans.PropertyValue; +import com.sun.star.beans.XPropertySet; +import com.sun.star.container.XNameContainer; +import com.sun.star.container.XSet; +import com.sun.star.document.XDocumentEventListener; +import com.sun.star.document.XEmbeddedScripts; +import com.sun.star.document.XEventsSupplier; +import com.sun.star.lang.XComponent; +import com.sun.star.frame.XComponentLoader; +import com.sun.star.frame.XDispatch; +import com.sun.star.frame.XDispatchProvider; +import com.sun.star.frame.XFrame; +import com.sun.star.frame.XGlobalEventBroadcaster; +import com.sun.star.frame.XLoadable; +import com.sun.star.frame.XModel; +import com.sun.star.frame.XModel2; +import com.sun.star.frame.XTitle; +import com.sun.star.frame.theGlobalEventBroadcaster; +import com.sun.star.lang.EventObject; +import com.sun.star.lang.XServiceInfo; +import com.sun.star.lang.XSingleComponentFactory; +import com.sun.star.lang.XTypeProvider; +import com.sun.star.script.provider.XScriptProviderSupplier; +import com.sun.star.sdb.XDocumentDataSource; + +import com.sun.star.sdb.XFormDocumentsSupplier; +import com.sun.star.sdb.XOfficeDatabaseDocument; +import com.sun.star.sdb.XReportDocumentsSupplier; +import com.sun.star.task.DocumentMacroConfirmationRequest; +import com.sun.star.task.XInteractionApprove; +import com.sun.star.task.XInteractionContinuation; +import com.sun.star.task.XInteractionHandler; +import com.sun.star.uno.XComponentContext; +import com.sun.star.util.CloseVetoException; +import com.sun.star.util.URL; +import com.sun.star.util.XChangesBatch; +import com.sun.star.util.XCloseable; +import com.sun.star.util.XModifiable; +import com.sun.star.util.XURLTransformer; +import java.io.IOException; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +// ---------- junit imports ----------------- +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + + +public class DatabaseDocument extends TestCase implements com.sun.star.document.XDocumentEventListener +{ + + private static final String _BLANK = "_blank"; + private XComponent m_callbackFactory = null; + private final ArrayList<String> m_documentEvents = new ArrayList<String>(); + private final ArrayList<String> m_globalEvents = new ArrayList<String>(); + // for those states, see testDocumentEvents + private static short STATE_NOT_STARTED = 0; + private static short STATE_LOADING_DOC = 1; + private static short STATE_MACRO_EXEC_APPROVED = 2; + private static short STATE_ON_LOAD_RECEIVED = 3; + private short m_loadDocState = STATE_NOT_STARTED; + + + /** a helper class which can be used by the Basic scripts in our test documents + * to notify us of events in this document + */ + private class CallbackComponent implements XDocumentEventListener, XTypeProvider + { + + public void documentEventOccured(DocumentEvent _event) + { + onDocumentEvent(_event); + } + + public void disposing(com.sun.star.lang.EventObject _Event) + { + // not interested in + } + + public Type[] getTypes() + { + final Class interfaces[] = getClass().getInterfaces(); + Type types[] = new Type[interfaces.length]; + for (int i = 0; i < interfaces.length; ++i) + { + types[i] = new Type(interfaces[i]); + } + return types; + } + + public byte[] getImplementationId() + { + return new byte[0]; + } + } + + + private static String getCallbackComponentServiceName() + { + return "org.openoffice.complex.dbaccess.EventCallback"; + } + + + /** a factory for a CallbackComponent + */ + private class CallbackComponentFactory implements XSingleComponentFactory, XServiceInfo, XComponent + { + + private final ArrayList<XEventListener> m_eventListeners = new ArrayList<XEventListener>(); + + public Object createInstanceWithContext(XComponentContext _context) throws com.sun.star.uno.Exception + { + return new CallbackComponent(); + } + + public Object createInstanceWithArgumentsAndContext(Object[] arg0, XComponentContext _context) throws com.sun.star.uno.Exception + { + return createInstanceWithContext(_context); + } + + public String getImplementationName() + { + return "org.openoffice.complex.dbaccess.CallbackComponent"; + } + + public boolean supportsService(String _service) + { + return _service.equals(getCallbackComponentServiceName()); + } + + public String[] getSupportedServiceNames() + { + return new String[] + { + getCallbackComponentServiceName() + }; + } + + public void dispose() + { + final EventObject event = new EventObject(this); + + final ArrayList<XEventListener> eventListenersCopy = (ArrayList<XEventListener>)m_eventListeners.clone(); + final Iterator<XEventListener> iter = eventListenersCopy.iterator(); + while (iter.hasNext()) + { + iter.next().disposing(event); + } + } + + public void addEventListener(XEventListener _listener) + { + if (_listener != null) + { + m_eventListeners.add(_listener); + } + } + + public void removeEventListener(XEventListener _listener) + { + m_eventListeners.remove(_listener); + } + } + + + private class MacroExecutionApprove implements XInteractionHandler + { + + private XInteractionHandler m_defaultHandler = null; + + MacroExecutionApprove(XMultiServiceFactory _factory) + { + try + { + m_defaultHandler = UnoRuntime.queryInterface(XInteractionHandler.class, _factory.createInstance("com.sun.star.task.InteractionHandler")); + } + catch (Exception ex) + { + Logger.getLogger(DatabaseDocument.class.getName()).log(Level.SEVERE, null, ex); + } + } + + public void handle(XInteractionRequest _request) + { + final Object request = _request.getRequest(); + if (!(request instanceof DocumentMacroConfirmationRequest) && (m_defaultHandler != null)) + { + m_defaultHandler.handle(_request); + return; + } + + assertEquals("interaction handler called in wrong state", STATE_LOADING_DOC, m_loadDocState); + + // auto-approve + final XInteractionContinuation continuations[] = _request.getContinuations(); + for (int i = 0; i < continuations.length; ++i) + { + final XInteractionApprove approve = UnoRuntime.queryInterface(XInteractionApprove.class, continuations[i]); + if (approve != null) + { + approve.select(); + m_loadDocState = STATE_MACRO_EXEC_APPROVED; + break; + } + } + } + } + + + + @Override + @Before + public void before() throws java.lang.Exception + { + super.before(); + + try + { + // at our service factory, insert a new factory for our CallbackComponent + // this will allow the Basic code in our test documents to call back into this test case + // here, by just instantiating this service + final XSet globalFactory = UnoRuntime.queryInterface(XSet.class, getMSF()); + m_callbackFactory = new CallbackComponentFactory(); + globalFactory.insert(m_callbackFactory); + + // register ourself as listener at the global event broadcaster + final XGlobalEventBroadcaster broadcaster + = theGlobalEventBroadcaster.get(getComponentContext()); + broadcaster.addDocumentEventListener(this); + } + catch (Exception e) + { + System.out.println("could not create the test case, error message:\n" + e.getMessage()); + e.printStackTrace(System.err); + fail("failed to create the test case"); + } + } + + + @Override + @After + public void after() throws java.lang.Exception + { + try + { + // dispose our callback factory. This will automatically remove it from our service + // factory + m_callbackFactory.dispose(); + + // revoke ourself as listener at the global event broadcaster + final XGlobalEventBroadcaster broadcaster + = theGlobalEventBroadcaster.get(getComponentContext()); + broadcaster.removeDocumentEventListener(this); + } + catch (Exception e) + { + System.out.println("could not create the test case, error message:\n" + e.getMessage()); + e.printStackTrace(System.err); + fail("failed to close the test case"); + } + + super.after(); + } + + + private static class UnoMethodDescriptor + { + + public Class unoInterfaceClass = null; + public String methodName = null; + + UnoMethodDescriptor(Class _class, String _method) + { + unoInterfaceClass = _class; + methodName = _method; + } + } + + + private void impl_checkDocumentInitState(Object _document, boolean _isInitialized) + { + // things you cannot do with an uninitialized document: + final UnoMethodDescriptor[] unsupportedMethods = new UnoMethodDescriptor[] + { + new UnoMethodDescriptor(XStorable.class, "store"), + new UnoMethodDescriptor(XFormDocumentsSupplier.class, "getFormDocuments"), + new UnoMethodDescriptor(XReportDocumentsSupplier.class, "getReportDocuments"), + new UnoMethodDescriptor(XScriptProviderSupplier.class, "getScriptProvider"), + new UnoMethodDescriptor(XEventsSupplier.class, "getEvents"), + new UnoMethodDescriptor(XTitle.class, "getTitle"), + new UnoMethodDescriptor(XModel2.class, "getControllers") + // (there's much more than this, but we cannot list all methods here, can we ...) + }; + + for (int i = 0; i < unsupportedMethods.length; ++i) + { + assureException( _document, unsupportedMethods[i].unoInterfaceClass, + unsupportedMethods[i].methodName, new Object[]{}, _isInitialized ? null : NotInitializedException.class ); + } + } + + + private XModel impl_createDocument() throws Exception + { + final XModel databaseDoc = UnoRuntime.queryInterface(XModel.class, getMSF().createInstance("com.sun.star.sdb.OfficeDatabaseDocument")); + + // should not be initialized here - we did neither initNew nor load nor storeAsURL it + impl_checkDocumentInitState(databaseDoc, false); + + return databaseDoc; + } + + + private void impl_closeDocument(XModel _databaseDoc) throws CloseVetoException, Exception + { + final XCloseable closeDoc = UnoRuntime.queryInterface(XCloseable.class, _databaseDoc); + closeDoc.close(true); + } + + + private XModel impl_createEmptyEmbeddedHSQLDocument() throws Exception, IOException + { + final XModel databaseDoc = UnoRuntime.queryInterface(XModel.class, getMSF().createInstance("com.sun.star.sdb.OfficeDatabaseDocument")); + final XStorable storeDoc = UnoRuntime.queryInterface(XStorable.class, databaseDoc); + + // verify the document rejects API calls which require it to be initialized + impl_checkDocumentInitState(databaseDoc, false); + + // though the document is not initialized, you can ask for the location, the URL, and the args + final String location = storeDoc.getLocation(); + final String url = databaseDoc.getURL(); + final PropertyValue[] args = databaseDoc.getArgs(); + // they should be all empty at this time + assertEquals("location is expected to be empty here", "", location); + assertEquals("URL is expected to be empty here", "", url); + assertEquals("Args are expected to be empty here", 0, args.length); + + // and, you should be able to set properties at the data source + final XOfficeDatabaseDocument dataSourceAccess = UnoRuntime.queryInterface(XOfficeDatabaseDocument.class, databaseDoc); + final XPropertySet dsProperties = UnoRuntime.queryInterface(XPropertySet.class, dataSourceAccess.getDataSource()); + dsProperties.setPropertyValue("URL", "sdbc:embedded:hsqldb"); + + final String documentURL = createTempFileURL(); + storeDoc.storeAsURL(documentURL, new PropertyValue[0]); + + // now that the document is stored, ... + // ... its URL should be correct + assertEquals("wrong URL after storing the document", documentURL, databaseDoc.getURL()); + // ... it should be initialized + impl_checkDocumentInitState(databaseDoc, true); + + return databaseDoc; + } + + + @Test + public void testLoadable() throws Exception, IOException + { + XModel databaseDoc = impl_createEmptyEmbeddedHSQLDocument(); + String documentURL = databaseDoc.getURL(); + + // there's three methods how you can initialize a database document: + + + // 1. XStorable::storeAsURL + // (this is for compatibility reasons, to not break existing code) + // this test is already made in impl_createEmptyEmbeddedHSQLDocument + + + // 2. XLoadable::load + databaseDoc = UnoRuntime.queryInterface(XModel.class, getMSF().createInstance("com.sun.star.sdb.OfficeDatabaseDocument")); + documentURL = copyToTempFile(documentURL); + // load the doc, and verify it's initialized then, and has the proper URL + XLoadable loadDoc = UnoRuntime.queryInterface(XLoadable.class, databaseDoc); + loadDoc.load(new PropertyValue[] + { + new PropertyValue("URL", 0, documentURL, PropertyState.DIRECT_VALUE) + }); + databaseDoc.attachResource(documentURL, new PropertyValue[0]); + + assertEquals("wrong URL after loading the document", documentURL, databaseDoc.getURL()); + impl_checkDocumentInitState(databaseDoc, true); + + // and while we are here ... initializing the same document again should not be possible + assureException( databaseDoc, XLoadable.class, "initNew", new Object[0], + DoubleInitializationException.class ); + assureException( databaseDoc, XLoadable.class, "load", new Object[] { new PropertyValue[0] }, + DoubleInitializationException.class ); + + + // 3. XLoadable::initNew + impl_closeDocument(databaseDoc); + databaseDoc = impl_createDocument(); + loadDoc = UnoRuntime.queryInterface(XLoadable.class, databaseDoc); + loadDoc.initNew(); + assertEquals("wrong URL after initializing the document", "", databaseDoc.getURL()); + impl_checkDocumentInitState(databaseDoc, true); + + // same as above - initializing the document a second time must fail + assureException( databaseDoc, XLoadable.class, "initNew", new Object[0], + DoubleInitializationException.class ); + assureException( databaseDoc, XLoadable.class, "load", new Object[] { new PropertyValue[0] }, + DoubleInitializationException.class ); + } + + + private PropertyValue[] impl_getMarkerLoadArgs() + { + return new PropertyValue[] + { + new PropertyValue( "PickListEntry", 0, false, PropertyState.DIRECT_VALUE ), + new PropertyValue( "TestCase_Marker", 0, "Yes", PropertyState.DIRECT_VALUE ) + }; + } + + + private boolean impl_hasMarker( final PropertyValue[] _args ) + { + for ( int i=0; i<_args.length; ++i ) + { + if ( _args[i].Name.equals( "TestCase_Marker" ) && _args[i].Value.equals( "Yes" ) ) + { + return true; + } + } + return false; + } + + + private PropertyValue[] impl_getDefaultLoadArgs() + { + return new PropertyValue[] + { + new PropertyValue("PickListEntry", 0, false, PropertyState.DIRECT_VALUE) + }; + } + + + private PropertyValue[] impl_getMacroExecLoadArgs() + { + return new PropertyValue[] + { + new PropertyValue("PickListEntry", 0, false, PropertyState.DIRECT_VALUE), + new PropertyValue("MacroExecutionMode", 0, com.sun.star.document.MacroExecMode.USE_CONFIG, PropertyState.DIRECT_VALUE), + new PropertyValue("InteractionHandler", 0, new MacroExecutionApprove(getMSF()), PropertyState.DIRECT_VALUE) + }; + } + + + private int impl_setMacroSecurityLevel(int _level) throws Exception + { + final XMultiServiceFactory configProvider = theDefaultProvider.get( + getComponentContext()); + + final NamedValue[] args = new NamedValue[] + { + new NamedValue("nodepath", "/org.openoffice.Office.Common/Security/Scripting") + }; + + final XPropertySet securitySettings = UnoRuntime.queryInterface(XPropertySet.class, configProvider.createInstanceWithArguments("com.sun.star.configuration.ConfigurationUpdateAccess", args)); + final int oldValue = ((Integer) securitySettings.getPropertyValue("MacroSecurityLevel")).intValue(); + securitySettings.setPropertyValue("MacroSecurityLevel", Integer.valueOf(_level)); + + final XChangesBatch committer = UnoRuntime.queryInterface(XChangesBatch.class, securitySettings); + committer.commitChanges(); + + return oldValue; + } + + + private XModel impl_loadDocument( final String _documentURL, final PropertyValue[] _loadArgs ) throws Exception + { + final XComponentLoader loader = UnoRuntime.queryInterface(XComponentLoader.class, getMSF().createInstance("com.sun.star.frame.Desktop")); + return UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(_documentURL, _BLANK, 0, _loadArgs)); + } + + + private void impl_storeDocument( final XModel _document ) throws Exception + { + // store the document + FileHelper.getOOoCompatibleFileURL( _document.getURL() ); + final XStorable storeDoc = UnoRuntime.queryInterface(XStorable.class, _document); + storeDoc.store(); + + } + + + private XModel impl_createDocWithMacro( final String _libName, final String _moduleName, final String _code ) throws Exception, IOException + { + // create an empty document + XModel databaseDoc = impl_createEmptyEmbeddedHSQLDocument(); + + // create Basic library/module therein + final XEmbeddedScripts embeddedScripts = UnoRuntime.queryInterface(XEmbeddedScripts.class, databaseDoc); + final XStorageBasedLibraryContainer basicLibs = embeddedScripts.getBasicLibraries(); + final XNameContainer newLib = basicLibs.createLibrary( _libName ); + newLib.insertByName( _moduleName, _code ); + + return databaseDoc; + } + + + /** Tests various aspects of database document "revenants" + * + * Well, I do not really have a good term for this... The point is, database documents are in real + * only *one* aspect of a more complex thing. The second aspect is a data source. Both, in some sense, + * just represent different views on the same thing. For a given database, there's at each time at most + * one data source, and at most one database document. Both have an independent life time, and are + * created when needed. + * In particular, a document can be closed (this is what happens when the last UI window displaying + * this document is closed), and then dies. Now when the other "view", the data source, still exists, + * the underlying document data is not discarded, but kept alive (else the data source would die + * just because the document dies, which is not desired). If the document is loaded, again, then + * it is re-created, using the data of its previous "incarnation". + * + * This method here tests some of those aspects of a document which should survive the death of one + * instance and re-creation as a revenant. + */ + @Test + public void testDocumentRevenants() throws Exception, IOException + { + // create an empty document + XModel databaseDoc = impl_createDocWithMacro( "Lib", "Module", + "Sub Hello\n" + + " MsgBox \"Hello\"\n" + + "End Sub\n" + ); + impl_storeDocument( databaseDoc ); + final String documentURL = databaseDoc.getURL(); + + // at this stage, the marker should not yet be present in the doc's args, else some of the below + // tests become meaningless + assertTrue( "A newly created doc should not have the test case marker", !impl_hasMarker( databaseDoc.getArgs() ) ); + + // obtain the DataSource associated with the document. Keeping this alive + // ensures that the "impl data" of the document is kept alive, too, so when closing + // and re-opening it, this "impl data" must be re-used. + XDocumentDataSource dataSource = UnoRuntime.queryInterface(XDocumentDataSource.class, UnoRuntime.queryInterface(XOfficeDatabaseDocument.class, databaseDoc).getDataSource()); + + // close and reload the doc + impl_closeDocument(databaseDoc); + databaseDoc = impl_loadDocument( documentURL, impl_getMarkerLoadArgs() ); + // since we just put the marker into the load-call, it should be present at the doc + assertTrue( "The test case marker got lost.", impl_hasMarker( databaseDoc.getArgs() ) ); + + // The basic library should have survived + final XEmbeddedScripts embeddedScripts = UnoRuntime.queryInterface(XEmbeddedScripts.class, databaseDoc); + final XStorageBasedLibraryContainer basicLibs = embeddedScripts.getBasicLibraries(); + assertTrue( "Basic lib did not survive reloading a closed document", basicLibs.hasByName( "Lib" ) ); + final XNameContainer lib = UnoRuntime.queryInterface(XNameContainer.class, basicLibs.getByName("Lib")); + assertTrue( "Basic module did not survive reloading a closed document", lib.hasByName( "Module" ) ); + + // now closing the doc, and obtaining it from the data source, should preserve the marker we put into the load + // args + impl_closeDocument( databaseDoc ); + databaseDoc = UnoRuntime.queryInterface(XModel.class, dataSource.getDatabaseDocument()); + assertTrue( "The test case marker did not survive re-retrieval of the doc from the data source.", + impl_hasMarker( databaseDoc.getArgs() ) ); + + // on the other hand, closing and regularly re-loading the doc *without* the marker should indeed + // lose it + impl_closeDocument( databaseDoc ); + databaseDoc = impl_loadDocument( documentURL, impl_getDefaultLoadArgs() ); + assertTrue( "Reloading the document kept the old args, instead of the newly supplied ones.", + !impl_hasMarker( databaseDoc.getArgs() ) ); + + // clean up + impl_closeDocument( databaseDoc ); + } + + + @Test + public void testDocumentEvents() throws Exception, IOException + { + // create an empty document + final String libName = "EventHandlers"; + final String moduleName = "all"; + final String eventHandlerCode = + "Option Explicit\n" + + "\n" + + "Sub OnLoad\n" + + " Dim oCallback as Object\n" + + " oCallback = createUnoService( \"" + getCallbackComponentServiceName() + "\" )\n" + + "\n" + + " ' as long as the Document is not passed to the Basic callbacks, we need to create\n" + + " ' one ourself\n" + + " Dim oEvent as new com.sun.star.document.DocumentEvent\n" + + " oEvent.EventName = \"OnLoad\"\n" + + " oEvent.Source = ThisComponent\n" + + "\n" + + " oCallback.documentEventOccurred( oEvent )\n" + + "End Sub\n"; + XModel databaseDoc = impl_createDocWithMacro( libName, moduleName, eventHandlerCode ); + final String documentURL = databaseDoc.getURL(); + + // bind the macro to the OnLoad event + final String macroURI = "vnd.sun.star.script:" + libName + "." + moduleName + ".OnLoad?language=Basic&location=document"; + final XEventsSupplier eventsSupplier = UnoRuntime.queryInterface(XEventsSupplier.class, databaseDoc); + eventsSupplier.getEvents().replaceByName("OnLoad", new PropertyValue[] + { + new PropertyValue("EventType", 0, "Script", PropertyState.DIRECT_VALUE), + new PropertyValue("Script", 0, macroURI, PropertyState.DIRECT_VALUE) + }); + + // store the document, and close it + impl_storeDocument( databaseDoc ); + impl_closeDocument( databaseDoc ); + + // ensure the macro security configuration is "ask the user for document macro execution" + final int oldSecurityLevel = impl_setMacroSecurityLevel(1); + + // load it, again + m_loadDocState = STATE_LOADING_DOC; + // expected order of states is: + // STATE_LOADING_DOC - initialized here + // STATE_MACRO_EXEC_APPROVED - done in our interaction handler, which auto-approves the execution of macros + // STATE_ON_LOAD_RECEIVED - done in our callback for the document events + // + // In particular, it is important that the interaction handler (which plays the role of the user confirmation + // here) is called before the OnLoad notification is received - since the latter happens from within + // a Basic macro which is bound to the OnLoad event of the document. + + final String context = "OnLoad"; + impl_startObservingEvents(context); + databaseDoc = impl_loadDocument( documentURL, impl_getMacroExecLoadArgs() ); + impl_stopObservingEvents(m_documentEvents, new String[] + { + "OnLoad" + }, context); + + assertEquals("our provided interaction handler was not called", STATE_ON_LOAD_RECEIVED, m_loadDocState); + + // restore macro security level + impl_setMacroSecurityLevel(oldSecurityLevel); + + // close the document + impl_closeDocument(databaseDoc); + } + + + @Test + public void testGlobalEvents() throws Exception, IOException + { + XModel databaseDoc = impl_createEmptyEmbeddedHSQLDocument(); + final XStorable storeDoc = UnoRuntime.queryInterface(XStorable.class, databaseDoc); + + String context, newURL; + + // XStorable.store + final String oldURL = databaseDoc.getURL(); + context = "store"; + impl_startObservingEvents(context); + storeDoc.store(); + assertEquals("store is not expected to change the document URL", databaseDoc.getURL(), oldURL); + impl_stopObservingEvents(m_globalEvents, new String[] + { + "OnSave", "OnSaveDone" + }, context); + + // XStorable.storeToURL + context = "storeToURL"; + impl_startObservingEvents(context); + storeDoc.storeToURL(createTempFileURL(), new PropertyValue[0]); + assertEquals("storetoURL is not expected to change the document URL", databaseDoc.getURL(), oldURL); + impl_stopObservingEvents(m_globalEvents, new String[] + { + "OnSaveTo", "OnSaveToDone" + }, context); + + // XStorable.storeAsURL + newURL = createTempFileURL(); + context = "storeAsURL"; + impl_startObservingEvents(context); + storeDoc.storeAsURL(newURL, new PropertyValue[0]); + assertEquals("storeAsURL is expected to change the document URL", databaseDoc.getURL(), newURL); + impl_stopObservingEvents(m_globalEvents, new String[] + { + "OnSaveAs", "OnSaveAsDone" + }, context); + + // XModifiable.setModified + final XModifiable modifyDoc = UnoRuntime.queryInterface(XModifiable.class, databaseDoc); + context = "setModified"; + impl_startObservingEvents(context); + modifyDoc.setModified(true); + assertEquals("setModified didn't work", modifyDoc.isModified(), true); + impl_stopObservingEvents(m_globalEvents, new String[] + { + "OnModifyChanged" + }, context); + + // XStorable.store, with implicit reset of the "Modified" flag + context = "store (2)"; + impl_startObservingEvents(context); + storeDoc.store(); + assertEquals("'store' should implicitly reset the modified flag", modifyDoc.isModified(), false); + impl_stopObservingEvents(m_globalEvents, new String[] + { + "OnSave", "OnSaveDone", "OnModifyChanged" + }, context); + + // XComponentLoader.loadComponentFromURL + newURL = copyToTempFile(databaseDoc.getURL()); + final XComponentLoader loader = UnoRuntime.queryInterface(XComponentLoader.class, getMSF().createInstance("com.sun.star.frame.Desktop")); + context = "loadComponentFromURL"; + impl_startObservingEvents(context); + databaseDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(newURL, _BLANK, 0, impl_getDefaultLoadArgs())); + impl_stopObservingEvents(m_globalEvents, + new String[] + { + "OnLoadFinished", "OnViewCreated", "OnFocus", "OnLoad" + }, context); + + // closing a document by API + final XCloseable closeDoc = UnoRuntime.queryInterface(XCloseable.class, databaseDoc); + context = "close (API)"; + impl_startObservingEvents(context); + closeDoc.close(true); + impl_stopObservingEvents(m_globalEvents, + new String[] + { + "OnPrepareUnload", "OnViewClosed", "OnUnload" + }, context); + + // closing a document via UI + context = "close (UI)"; + impl_startObservingEvents("prepare for '" + context + "'"); + databaseDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(newURL, _BLANK, 0, impl_getDefaultLoadArgs())); + impl_waitForEvent(m_globalEvents, "OnLoad", 5000); + // wait for all events to arrive - OnLoad should be the last one + + final XDispatchProvider dispatchProvider = UnoRuntime.queryInterface(XDispatchProvider.class, databaseDoc.getCurrentController().getFrame()); + final URL url = impl_getURL(".uno:CloseDoc"); + final XDispatch dispatcher = dispatchProvider.queryDispatch(url, "", 0); + impl_startObservingEvents(context); + dispatcher.dispatch(url, new PropertyValue[0]); + impl_stopObservingEvents(m_globalEvents, + new String[] + { + "OnPrepareViewClosing", "OnViewClosed", "OnPrepareUnload", "OnUnload" + }, context); + + // creating a new document + databaseDoc = impl_createDocument(); + final XLoadable loadDoc = UnoRuntime.queryInterface(XLoadable.class, databaseDoc); + context = "initNew"; + impl_startObservingEvents(context); + loadDoc.initNew(); + impl_stopObservingEvents(m_globalEvents, new String[] + { + "OnCreate" + }, context); + + impl_startObservingEvents(context + " (cleanup)"); + impl_closeDocument(databaseDoc); + impl_waitForEvent(m_globalEvents, "OnUnload", 5000); + + // focus changes + context = "activation"; + // for this, load a database document ... + impl_startObservingEvents("prepare for '" + context + "'"); + databaseDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(newURL, _BLANK, 0, impl_getDefaultLoadArgs())); + final int previousOnLoadEventPos = impl_waitForEvent(m_globalEvents, "OnLoad", 5000); + // ... and another document ... + final String otherURL = copyToTempFile(databaseDoc.getURL()); + final XModel otherDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(otherURL, _BLANK, 0, impl_getDefaultLoadArgs())); + impl_raise(otherDoc); + impl_waitForEvent(m_globalEvents, "OnLoad", 5000, previousOnLoadEventPos + 1); + + // ... and switch between the two + impl_startObservingEvents(context); + impl_raise(databaseDoc); + impl_stopObservingEvents(m_globalEvents, new String[] + { + "OnUnfocus", "OnFocus" + }, context); + + // cleanup + impl_startObservingEvents("cleanup after '" + context + "'"); + impl_closeDocument(databaseDoc); + impl_closeDocument(otherDoc); + } + + + private URL impl_getURL(String _completeURL) throws Exception + { + final URL[] url = + { + new URL() + }; + url[0].Complete = _completeURL; + final XURLTransformer urlTransformer = UnoRuntime.queryInterface(XURLTransformer.class, getMSF().createInstance("com.sun.star.util.URLTransformer")); + urlTransformer.parseStrict(url); + return url[0]; + } + + + private void impl_raise(XModel _document) + { + final XFrame frame = _document.getCurrentController().getFrame(); + final XTopWindow topWindow = UnoRuntime.queryInterface(XTopWindow.class, frame.getContainerWindow()); + topWindow.toFront(); + } + + + private void impl_startObservingEvents(String _context) + { + System.out.println(" " + _context + " {"); + synchronized (m_documentEvents) + { + m_documentEvents.clear(); + } + synchronized (m_globalEvents) + { + m_globalEvents.clear(); + } + } + + + private void impl_stopObservingEvents(ArrayList<String> _actualEvents, String[] _expectedEvents, String _context) + { + try + { + synchronized (_actualEvents) + { + int actualEventCount = _actualEvents.size(); + while (actualEventCount < _expectedEvents.length) + { + // well, it's possible not all events already arrived, yet - finally, some of them + // are notified asynchronously + // So, wait a few seconds. + try + { + _actualEvents.wait(20000); + } + catch (InterruptedException ex) + { + } + + if (actualEventCount == _actualEvents.size()) + // the above wait was left because of the timeout, *not* because an event + // arrived. Okay, we won't wait any longer, this is a failure. + { + break; + } + actualEventCount = _actualEvents.size(); + } + + assertEquals("wrong event count for '" + _context + "'", + _expectedEvents.length, _actualEvents.size()); + + for (int i = 0; i < _expectedEvents.length; ++i) + { + assertEquals("wrong event at position " + (i + 1) + " for '" + _context + "'", + _expectedEvents[i], _actualEvents.get(i)); + } + } + } + finally + { + System.out.println(" }"); + } + } + + + private int impl_waitForEvent(ArrayList<String> _eventQueue, String _expectedEvent, int _maxMilliseconds) + { + return impl_waitForEvent(_eventQueue, _expectedEvent, _maxMilliseconds, 0); + } + + + private int impl_waitForEvent(ArrayList<String> _eventQueue, String _expectedEvent, int _maxMilliseconds, int _firstQueueElementToCheck) + { + synchronized (_eventQueue) + { + int waitedMilliseconds = 0; + + while (waitedMilliseconds < _maxMilliseconds) + { + for (int i = _firstQueueElementToCheck; i < _eventQueue.size(); ++i) + { + if (_expectedEvent.equals(_eventQueue.get(i))) + // found the event in the queue + { + return i; + } + } + + // wait a little, perhaps the event will still arrive + try + { + _eventQueue.wait(500); + waitedMilliseconds += 500; + } + catch (InterruptedException e) + { + } + } + } + + fail("expected event '" + _expectedEvent + "' did not arrive after " + _maxMilliseconds + " milliseconds"); + return -1; + } + + + private void onDocumentEvent(DocumentEvent _Event) + { + if ("OnTitleChanged".equals(_Event.EventName)) + // OnTitleChanged events are notified too often. This is known, and accepted. + // (the deeper reason is that it's difficult to determine, in the DatabaseDocument implementation, + // when the title actually changed. In particular, when we do a saveAsURL, and then ask for a + // title *before* the TitleHelper got the document's OnSaveAsDone event, then the wrong (old) + // title is obtained. + { + return; + } + + if ((_Event.EventName.equals("OnLoad")) && (m_loadDocState != STATE_NOT_STARTED)) + { + assertEquals("OnLoad event must come *after* invocation of the interaction handler / user!", + m_loadDocState, STATE_MACRO_EXEC_APPROVED); + m_loadDocState = STATE_ON_LOAD_RECEIVED; + } + + synchronized (m_documentEvents) + { + m_documentEvents.add(_Event.EventName); + m_documentEvents.notifyAll(); + } + + System.out.println(" document event: " + _Event.EventName); + } + + + public void documentEventOccured(DocumentEvent _Event) + { + if ("OnTitleChanged".equals(_Event.EventName)) + // ignore. See onDocumentEvent for a justification + { + return; + } + + synchronized (m_globalEvents) + { + m_globalEvents.add(_Event.EventName); + m_globalEvents.notifyAll(); + } + + System.out.println(" global event: " + _Event.EventName); + } + + + public void disposing(EventObject _Event) + { + // not interested in + } +} |