1 /*************************************************************************
2  *
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * Copyright 2000, 2010 Oracle and/or its affiliates.
6  *
7  * OpenOffice.org - a multi-platform office productivity suite
8  *
9  * This file is part of OpenOffice.org.
10  *
11  * OpenOffice.org is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU Lesser General Public License version 3
13  * only, as published by the Free Software Foundation.
14  *
15  * OpenOffice.org is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Lesser General Public License version 3 for more details
19  * (a copy is included in the LICENSE file that accompanied this code).
20  *
21  * You should have received a copy of the GNU Lesser General Public License
22  * version 3 along with OpenOffice.org.  If not, see
23  * <http://www.openoffice.org/license.html>
24  * for a copy of the LGPLv3 License.
25  *
26  ************************************************************************/
27 package complex.dbaccess;
28 
29 import com.sun.star.lang.NotInitializedException;
30 import com.sun.star.frame.DoubleInitializationException;
31 import com.sun.star.awt.XTopWindow;
32 import com.sun.star.beans.PropertyState;
33 import com.sun.star.document.DocumentEvent;
34 import com.sun.star.lang.XEventListener;
35 import com.sun.star.lang.XMultiServiceFactory;
36 import com.sun.star.script.XStorageBasedLibraryContainer;
37 import com.sun.star.task.XInteractionRequest;
38 
39 import com.sun.star.uno.Type;
40 import com.sun.star.uno.UnoRuntime;
41 import com.sun.star.frame.XStorable;
42 import com.sun.star.beans.PropertyValue;
43 import com.sun.star.beans.XPropertySet;
44 import com.sun.star.container.XNameContainer;
45 import com.sun.star.container.XSet;
46 import com.sun.star.document.XDocumentEventBroadcaster;
47 import com.sun.star.document.XDocumentEventListener;
48 import com.sun.star.document.XEmbeddedScripts;
49 import com.sun.star.document.XEventsSupplier;
50 import com.sun.star.lang.XComponent;
51 import com.sun.star.frame.XComponentLoader;
52 import com.sun.star.frame.XDispatch;
53 import com.sun.star.frame.XDispatchProvider;
54 import com.sun.star.frame.XFrame;
55 import com.sun.star.frame.XLoadable;
56 import com.sun.star.frame.XModel;
57 import com.sun.star.frame.XModel2;
58 import com.sun.star.frame.XTitle;
59 import com.sun.star.lang.EventObject;
60 import com.sun.star.lang.XServiceInfo;
61 import com.sun.star.lang.XSingleComponentFactory;
62 import com.sun.star.lang.XTypeProvider;
63 import com.sun.star.script.provider.XScriptProviderSupplier;
64 import com.sun.star.sdb.XDocumentDataSource;
65 
66 import com.sun.star.sdb.XFormDocumentsSupplier;
67 import com.sun.star.sdb.XOfficeDatabaseDocument;
68 import com.sun.star.sdb.XReportDocumentsSupplier;
69 import com.sun.star.task.DocumentMacroConfirmationRequest;
70 import com.sun.star.task.XInteractionApprove;
71 import com.sun.star.task.XInteractionContinuation;
72 import com.sun.star.task.XInteractionHandler;
73 import com.sun.star.uno.XComponentContext;
74 import com.sun.star.util.CloseVetoException;
75 import com.sun.star.util.URL;
76 import com.sun.star.util.XChangesBatch;
77 import com.sun.star.util.XCloseable;
78 import com.sun.star.util.XModifiable;
79 import com.sun.star.util.XURLTransformer;
80 import java.io.IOException;
81 import java.util.Iterator;
82 import java.util.ArrayList;
83 import java.util.logging.Level;
84 import java.util.logging.Logger;
85 
86 // ---------- junit imports -----------------
87 import org.junit.After;
88 import org.junit.Before;
89 import org.junit.Test;
90 import static org.junit.Assert.*;
91 // ------------------------------------------
92 
93 public class DatabaseDocument extends TestCase implements com.sun.star.document.XDocumentEventListener
94 {
95 
96     private static final String _BLANK = "_blank";
97     private XComponent m_callbackFactory = null;
98     private final ArrayList m_documentEvents = new ArrayList();
99     private final ArrayList m_globalEvents = new ArrayList();
100     // for those states, see testDocumentEvents
101     private static short STATE_NOT_STARTED = 0;
102     private static short STATE_LOADING_DOC = 1;
103     private static short STATE_MACRO_EXEC_APPROVED = 2;
104     private static short STATE_ON_LOAD_RECEIVED = 3;
105     private short m_loadDocState = STATE_NOT_STARTED;
106 
107     // ========================================================================================================
108     /** a helper class which can be used by the Basic scripts in our test documents
109      *  to notify us of events in this document
110      */
111     private class CallbackComponent implements XDocumentEventListener, XTypeProvider
112     {
113 
114         public void documentEventOccured(DocumentEvent _event)
115         {
116             onDocumentEvent(_event);
117         }
118 
119         public void disposing(com.sun.star.lang.EventObject _Event)
120         {
121             // not interested in
122         }
123 
124         public Type[] getTypes()
125         {
126             final Class interfaces[] = getClass().getInterfaces();
127             Type types[] = new Type[interfaces.length];
128             for (int i = 0; i < interfaces.length; ++i)
129             {
130                 types[i] = new Type(interfaces[i]);
131             }
132             return types;
133         }
134 
135         public byte[] getImplementationId()
136         {
137             return getClass().toString().getBytes();
138         }
139     };
140 
141     // ========================================================================================================
142     private static String getCallbackComponentServiceName()
143     {
144         return "org.openoffice.complex.dbaccess.EventCallback";
145     }
146 
147     // ========================================================================================================
148     /** a factory for a CallbackComponent
149      */
150     private class CallbackComponentFactory implements XSingleComponentFactory, XServiceInfo, XComponent
151     {
152 
153         private final ArrayList m_eventListeners = new ArrayList();
154 
155         public Object createInstanceWithContext(XComponentContext _context) throws com.sun.star.uno.Exception
156         {
157             return new CallbackComponent();
158         }
159 
160         public Object createInstanceWithArgumentsAndContext(Object[] arg0, XComponentContext _context) throws com.sun.star.uno.Exception
161         {
162             return createInstanceWithContext(_context);
163         }
164 
165         public String getImplementationName()
166         {
167             return "org.openoffice.complex.dbaccess.CallbackComponent";
168         }
169 
170         public boolean supportsService(String _service)
171         {
172             return _service.equals(getCallbackComponentServiceName());
173         }
174 
175         public String[] getSupportedServiceNames()
176         {
177             return new String[]
178                     {
179                         getCallbackComponentServiceName()
180                     };
181         }
182 
183         public void dispose()
184         {
185             final EventObject event = new EventObject(this);
186 
187             final ArrayList eventListenersCopy = (ArrayList) m_eventListeners.clone();
188             final Iterator iter = eventListenersCopy.iterator();
189             while (iter.hasNext())
190             {
191                 ((XEventListener) iter.next()).disposing(event);
192             }
193         }
194 
195         public void addEventListener(XEventListener _listener)
196         {
197             if (_listener != null)
198             {
199                 m_eventListeners.add(_listener);
200             }
201         }
202 
203         public void removeEventListener(XEventListener _listener)
204         {
205             m_eventListeners.remove(_listener);
206         }
207     };
208 
209     // ========================================================================================================
210     private class MacroExecutionApprove implements XInteractionHandler
211     {
212 
213         private XInteractionHandler m_defaultHandler = null;
214 
215         MacroExecutionApprove(XMultiServiceFactory _factory)
216         {
217             try
218             {
219                 m_defaultHandler = UnoRuntime.queryInterface(XInteractionHandler.class, _factory.createInstance("com.sun.star.task.InteractionHandler"));
220             }
221             catch (Exception ex)
222             {
223                 Logger.getLogger(DatabaseDocument.class.getName()).log(Level.SEVERE, null, ex);
224             }
225         }
226 
227         public void handle(XInteractionRequest _request)
228         {
229             final Object request = _request.getRequest();
230             if (!(request instanceof DocumentMacroConfirmationRequest) && (m_defaultHandler != null))
231             {
232                 m_defaultHandler.handle(_request);
233                 return;
234             }
235 
236             assertEquals("interaction handleer called in wrong state", STATE_LOADING_DOC, m_loadDocState);
237 
238             // auto-approve
239             final XInteractionContinuation continuations[] = _request.getContinuations();
240             for (int i = 0; i < continuations.length; ++i)
241             {
242                 final XInteractionApprove approve = UnoRuntime.queryInterface(XInteractionApprove.class, continuations[i]);
243                 if (approve != null)
244                 {
245                     approve.select();
246                     m_loadDocState = STATE_MACRO_EXEC_APPROVED;
247                     break;
248                 }
249             }
250         }
251     };
252 
253     // ========================================================================================================
254     // --------------------------------------------------------------------------------------------------------
255     @Before
256     public void before() throws java.lang.Exception
257     {
258         super.before();
259 
260         try
261         {
262             // at our service factory, insert a new factory for our CallbackComponent
263             // this will allow the Basic code in our test documents to call back into this test case
264             // here, by just instantiating this service
265             final XSet globalFactory = UnoRuntime.queryInterface(XSet.class, getMSF());
266             m_callbackFactory = new CallbackComponentFactory();
267             globalFactory.insert(m_callbackFactory);
268 
269             // register ourself as listener at the global event broadcaster
270             final XDocumentEventBroadcaster broadcaster = UnoRuntime.queryInterface(XDocumentEventBroadcaster.class, getMSF().createInstance("com.sun.star.frame.GlobalEventBroadcaster"));
271             broadcaster.addDocumentEventListener(this);
272         }
273         catch (Exception e)
274         {
275             System.out.println("could not create the test case, error message:\n" + e.getMessage());
276             e.printStackTrace(System.err);
277             fail("failed to create the test case");
278         }
279     }
280 
281     // --------------------------------------------------------------------------------------------------------
282     @After
283     public void after() throws java.lang.Exception
284     {
285         try
286         {
287             // dispose our callback factory. This will automatically remove it from our service
288             // factory
289             m_callbackFactory.dispose();
290 
291             // revoke ourself as listener at the global event broadcaster
292             final XDocumentEventBroadcaster broadcaster = UnoRuntime.queryInterface(XDocumentEventBroadcaster.class, getMSF().createInstance("com.sun.star.frame.GlobalEventBroadcaster"));
293             broadcaster.removeDocumentEventListener(this);
294         }
295         catch (Exception e)
296         {
297             System.out.println("could not create the test case, error message:\n" + e.getMessage());
298             e.printStackTrace(System.err);
299             fail("failed to close the test case");
300         }
301 
302         super.after();
303     }
304 
305     // --------------------------------------------------------------------------------------------------------
306     private static class UnoMethodDescriptor
307     {
308 
309         public Class unoInterfaceClass = null;
310         public String methodName = null;
311 
312         UnoMethodDescriptor(Class _class, String _method)
313         {
314             unoInterfaceClass = _class;
315             methodName = _method;
316         }
317     }
318 
319     // --------------------------------------------------------------------------------------------------------
320     private void impl_checkDocumentInitState(Object _document, boolean _isInitialized)
321     {
322         // things you cannot do with an uninitialized document:
323         final UnoMethodDescriptor[] unsupportedMethods = new UnoMethodDescriptor[]
324         {
325             new UnoMethodDescriptor(XStorable.class, "store"),
326             new UnoMethodDescriptor(XFormDocumentsSupplier.class, "getFormDocuments"),
327             new UnoMethodDescriptor(XReportDocumentsSupplier.class, "getReportDocuments"),
328             new UnoMethodDescriptor(XScriptProviderSupplier.class, "getScriptProvider"),
329             new UnoMethodDescriptor(XEventsSupplier.class, "getEvents"),
330             new UnoMethodDescriptor(XTitle.class, "getTitle"),
331             new UnoMethodDescriptor(XModel2.class, "getControllers")
332         // (there's much more than this, but we cannot list all methods here, can we ...)
333         };
334 
335         for (int i = 0; i < unsupportedMethods.length; ++i)
336         {
337             assureException( _document, unsupportedMethods[i].unoInterfaceClass,
338                 unsupportedMethods[i].methodName, new Object[]{}, _isInitialized ? null : NotInitializedException.class );
339         }
340     }
341 
342     // --------------------------------------------------------------------------------------------------------
343     private XModel impl_createDocument() throws Exception
344     {
345         final XModel databaseDoc = UnoRuntime.queryInterface(XModel.class, getMSF().createInstance("com.sun.star.sdb.OfficeDatabaseDocument"));
346 
347         // should not be initialized here - we did neither initNew nor load nor storeAsURL it
348         impl_checkDocumentInitState(databaseDoc, false);
349 
350         return databaseDoc;
351     }
352 
353     // --------------------------------------------------------------------------------------------------------
354     private void impl_closeDocument(XModel _databaseDoc) throws CloseVetoException, IOException, Exception
355     {
356         final XCloseable closeDoc = UnoRuntime.queryInterface(XCloseable.class, _databaseDoc);
357         closeDoc.close(true);
358     }
359 
360     // --------------------------------------------------------------------------------------------------------
361     private XModel impl_createEmptyEmbeddedHSQLDocument() throws Exception, IOException
362     {
363         final XModel databaseDoc = UnoRuntime.queryInterface(XModel.class, getMSF().createInstance("com.sun.star.sdb.OfficeDatabaseDocument"));
364         final XStorable storeDoc = UnoRuntime.queryInterface(XStorable.class, databaseDoc);
365 
366         // verify the document rejects API calls which require it to be initialized
367         impl_checkDocumentInitState(databaseDoc, false);
368 
369         // though the document is not initialized, you can ask for the location, the URL, and the args
370         final String location = storeDoc.getLocation();
371         final String url = databaseDoc.getURL();
372         final PropertyValue[] args = databaseDoc.getArgs();
373         // they should be all empty at this time
374         assertEquals("location is expected to be empty here", "", location);
375         assertEquals("URL is expected to be empty here", "", url);
376         assertEquals("Args are expected to be empty here", 0, args.length);
377 
378         // and, you should be able to set properties at the data source
379         final XOfficeDatabaseDocument dataSourceAccess = UnoRuntime.queryInterface(XOfficeDatabaseDocument.class, databaseDoc);
380         final XPropertySet dsProperties = UnoRuntime.queryInterface(XPropertySet.class, dataSourceAccess.getDataSource());
381         dsProperties.setPropertyValue("URL", "sdbc:embedded:hsqldb");
382 
383         final String documentURL = createTempFileURL();
384         storeDoc.storeAsURL(documentURL, new PropertyValue[0]);
385 
386         // now that the document is stored, ...
387         // ... its URL should be correct
388         assertEquals("wrong URL after storing the document", documentURL, databaseDoc.getURL());
389         // ... it should be initialized
390         impl_checkDocumentInitState(databaseDoc, true);
391 
392         return databaseDoc;
393     }
394 
395     // --------------------------------------------------------------------------------------------------------
396     @Test
397     public void testLoadable() throws Exception, IOException
398     {
399         XModel databaseDoc = impl_createEmptyEmbeddedHSQLDocument();
400         String documentURL = databaseDoc.getURL();
401 
402         // there's three methods how you can initialize a database document:
403 
404         // ....................................................................
405         // 1. XStorable::storeAsURL
406         //      (this is for compatibility reasons, to not break existing code)
407         // this test is already made in impl_createEmptyEmbeddedHSQLDocument
408 
409         // ....................................................................
410         // 2. XLoadable::load
411         databaseDoc = UnoRuntime.queryInterface(XModel.class, getMSF().createInstance("com.sun.star.sdb.OfficeDatabaseDocument"));
412         documentURL = copyToTempFile(documentURL);
413         // load the doc, and verify it's initialized then, and has the proper URL
414         XLoadable loadDoc = UnoRuntime.queryInterface(XLoadable.class, databaseDoc);
415         loadDoc.load(new PropertyValue[]
416                 {
417                     new PropertyValue("URL", 0, documentURL, PropertyState.DIRECT_VALUE)
418                 });
419         databaseDoc.attachResource(documentURL, new PropertyValue[0]);
420 
421         assertEquals("wrong URL after loading the document", documentURL, databaseDoc.getURL());
422         impl_checkDocumentInitState(databaseDoc, true);
423 
424         // and while we are here ... initilizing the same document again should not be possible
425         assureException( databaseDoc, XLoadable.class, "initNew", new Object[0],
426             DoubleInitializationException.class );
427         assureException( databaseDoc, XLoadable.class, "load", new Object[] { new PropertyValue[0] },
428             DoubleInitializationException.class );
429 
430         // ....................................................................
431         // 3. XLoadable::initNew
432         impl_closeDocument(databaseDoc);
433         databaseDoc = impl_createDocument();
434         loadDoc = UnoRuntime.queryInterface(XLoadable.class, databaseDoc);
435         loadDoc.initNew();
436         assertEquals("wrong URL after initializing the document", "", databaseDoc.getURL());
437         impl_checkDocumentInitState(databaseDoc, true);
438 
439         // same as above - initializing the document a second time must fail
440         assureException( databaseDoc, XLoadable.class, "initNew", new Object[0],
441             DoubleInitializationException.class );
442         assureException( databaseDoc, XLoadable.class, "load", new Object[] { new PropertyValue[0] },
443             DoubleInitializationException.class );
444     }
445 
446     // --------------------------------------------------------------------------------------------------------
447     private PropertyValue[] impl_getMarkerLoadArgs()
448     {
449         return new PropertyValue[]
450                 {
451                     new PropertyValue( "PickListEntry", 0, false, PropertyState.DIRECT_VALUE ),
452                     new PropertyValue( "TestCase_Marker", 0, "Yes", PropertyState.DIRECT_VALUE )
453                 };
454     }
455 
456     // --------------------------------------------------------------------------------------------------------
457     private boolean impl_hasMarker( final PropertyValue[] _args )
458     {
459         for ( int i=0; i<_args.length; ++i )
460         {
461             if ( _args[i].Name.equals( "TestCase_Marker" ) && _args[i].Value.equals( "Yes" ) )
462             {
463                 return true;
464             }
465         }
466         return false;
467     }
468 
469     // --------------------------------------------------------------------------------------------------------
470     private PropertyValue[] impl_getDefaultLoadArgs()
471     {
472         return new PropertyValue[]
473                 {
474                     new PropertyValue("PickListEntry", 0, false, PropertyState.DIRECT_VALUE)
475                 };
476     }
477 
478     // --------------------------------------------------------------------------------------------------------
479     private PropertyValue[] impl_getMacroExecLoadArgs()
480     {
481         return new PropertyValue[]
482                 {
483                     new PropertyValue("PickListEntry", 0, false, PropertyState.DIRECT_VALUE),
484                     new PropertyValue("MacroExecutionMode", 0, com.sun.star.document.MacroExecMode.USE_CONFIG, PropertyState.DIRECT_VALUE),
485                     new PropertyValue("InteractionHandler", 0, new MacroExecutionApprove(getMSF()), PropertyState.DIRECT_VALUE)
486                 };
487     }
488 
489     // --------------------------------------------------------------------------------------------------------
490     private int impl_setMacroSecurityLevel(int _level) throws Exception
491     {
492         final XMultiServiceFactory configProvider = UnoRuntime.queryInterface(XMultiServiceFactory.class, getMSF().createInstance("com.sun.star.configuration.ConfigurationProvider"));
493 
494         final PropertyValue[] args = new PropertyValue[]
495         {
496             new PropertyValue("nodepath", 0, "/org.openoffice.Office.Common/Security/Scripting", PropertyState.DIRECT_VALUE)
497         };
498 
499         final XPropertySet securitySettings = UnoRuntime.queryInterface(XPropertySet.class, configProvider.createInstanceWithArguments("com.sun.star.configuration.ConfigurationUpdateAccess", args));
500         final int oldValue = ((Integer) securitySettings.getPropertyValue("MacroSecurityLevel")).intValue();
501         securitySettings.setPropertyValue("MacroSecurityLevel", Integer.valueOf(_level));
502 
503         final XChangesBatch committer = UnoRuntime.queryInterface(XChangesBatch.class, securitySettings);
504         committer.commitChanges();
505 
506         return oldValue;
507     }
508 
509     // --------------------------------------------------------------------------------------------------------
510     private XModel impl_loadDocument( final String _documentURL, final PropertyValue[] _loadArgs ) throws Exception
511     {
512         final XComponentLoader loader = UnoRuntime.queryInterface(XComponentLoader.class, getMSF().createInstance("com.sun.star.frame.Desktop"));
513         return UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(_documentURL, _BLANK, 0, _loadArgs));
514     }
515 
516     // --------------------------------------------------------------------------------------------------------
517     private void impl_storeDocument( final XModel _document ) throws Exception, IOException
518     {
519         // store the document
520         final String documentURL = FileHelper.getOOoCompatibleFileURL( _document.getURL() );
521         final XStorable storeDoc = UnoRuntime.queryInterface(XStorable.class, _document);
522         storeDoc.store();
523 
524     }
525 
526     // --------------------------------------------------------------------------------------------------------
527     private XModel impl_createDocWithMacro( final String _libName, final String _moduleName, final String _code ) throws Exception, IOException
528     {
529         // create an empty document
530         XModel databaseDoc = impl_createEmptyEmbeddedHSQLDocument();
531 
532         // create Basic library/module therein
533         final XEmbeddedScripts embeddedScripts = UnoRuntime.queryInterface(XEmbeddedScripts.class, databaseDoc);
534         final XStorageBasedLibraryContainer basicLibs = embeddedScripts.getBasicLibraries();
535         final XNameContainer newLib = basicLibs.createLibrary( _libName );
536         newLib.insertByName( _moduleName, _code );
537 
538         return databaseDoc;
539     }
540 
541     // --------------------------------------------------------------------------------------------------------
542     /** tests various aspects of database document "revenants"
543      *
544      *  Well, I do not really have a good term for this ... The point is, database documents are in real
545      *  only *one* aspect of a more complex thing. The second aspect is a data source. Both, in some sense,
546      *  just represent different views on the same thing. For a given database, there's at each time at most
547      *  one data source, and at most one database document. Both have a independent life time, and are
548      *  created when needed.
549      *  In particular, a document can be closed (this is what happens when the last UI window displaying
550      *  this document is closed), and then dies. Now when the other "view", the data source, still exists,
551      *  the the underlying document data is not discarded, but kept alive (else the data source would die
552      *  just because the document dies, which is not desired). If the document is loaded, again, then
553      *  it is re-created, using the data of its previous "incarnation".
554      *
555      *  This method here tests some of those aspects of a document which should survive the death of one
556      *  instance and re-creation as a revenant.
557     */
558     @Test
559     public void testDocumentRevenants() throws Exception, IOException
560     {
561         // create an empty document
562         XModel databaseDoc = impl_createDocWithMacro( "Lib", "Module",
563             "Sub Hello\n" +
564             "    MsgBox \"Hello\"\n" +
565             "End Sub\n"
566         );
567         impl_storeDocument( databaseDoc );
568         final String documentURL = databaseDoc.getURL();
569 
570         // at this stage, the marker should not yet be present in the doc's args, else some of the below
571         // tests become meaningless
572         assertTrue( "A newly created doc should not have the test case marker", !impl_hasMarker( databaseDoc.getArgs() ) );
573 
574         // obtain the DataSource associated with the document. Keeping this alive
575         // ensures that the "impl data" of the document is kept alive, too, so when closing
576         // and re-opening it, this "impl data" must be re-used.
577         XDocumentDataSource dataSource = UnoRuntime.queryInterface(XDocumentDataSource.class, (UnoRuntime.queryInterface(XOfficeDatabaseDocument.class, databaseDoc)).getDataSource());
578 
579         // close and reload the doc
580         impl_closeDocument(databaseDoc);
581         databaseDoc = impl_loadDocument( documentURL, impl_getMarkerLoadArgs() );
582         // since we just put the marker into the load-call, it should be present at the doc
583         assertTrue( "The test case marker got lost.", impl_hasMarker( databaseDoc.getArgs() ) );
584 
585         // The basic library should have survived
586         final XEmbeddedScripts embeddedScripts = UnoRuntime.queryInterface(XEmbeddedScripts.class, databaseDoc);
587         final XStorageBasedLibraryContainer basicLibs = embeddedScripts.getBasicLibraries();
588         assertTrue( "Baisc lib did not survive reloading a closed document", basicLibs.hasByName( "Lib" ) );
589         final XNameContainer lib = UnoRuntime.queryInterface(XNameContainer.class, basicLibs.getByName("Lib"));
590         assertTrue( "Basic module did not survive reloading a closed document", lib.hasByName( "Module" ) );
591 
592         // now closing the doc, and obtaining it from the data source, should preserve the marker we put into the load
593         // args
594         impl_closeDocument( databaseDoc );
595         databaseDoc = UnoRuntime.queryInterface(XModel.class, dataSource.getDatabaseDocument());
596         assertTrue( "The test case marker did not survive re-retrieval of the doc from the data source.",
597             impl_hasMarker( databaseDoc.getArgs() ) );
598 
599         // on the other hand, closing and regurlarly re-loading the doc *without* the marker should indeed
600         // lose it
601         impl_closeDocument( databaseDoc );
602         databaseDoc = impl_loadDocument( documentURL, impl_getDefaultLoadArgs() );
603         assertTrue( "Reloading the document kept the old args, instead of the newly supplied ones.",
604             !impl_hasMarker( databaseDoc.getArgs() ) );
605 
606         // clean up
607         impl_closeDocument( databaseDoc );
608     }
609 
610     // --------------------------------------------------------------------------------------------------------
611     @Test
612     public void testDocumentEvents() throws Exception, IOException
613     {
614         // create an empty document
615         final String libName = "EventHandlers";
616         final String moduleName = "all";
617         final String eventHandlerCode =
618                 "Option Explicit\n" +
619                 "\n" +
620                 "Sub OnLoad\n" +
621                 "  Dim oCallback as Object\n" +
622                 "  oCallback = createUnoService( \"" + getCallbackComponentServiceName() + "\" )\n" +
623                 "\n" +
624                 "  ' as long as the Document is not passed to the Basic callbacks, we need to create\n" +
625                 "  ' one ourself\n" +
626                 "  Dim oEvent as new com.sun.star.document.DocumentEvent\n" +
627                 "  oEvent.EventName = \"OnLoad\"\n" +
628                 "  oEvent.Source = ThisComponent\n" +
629                 "\n" +
630                 "  oCallback.documentEventOccured( oEvent )\n" +
631                 "End Sub\n";
632         XModel databaseDoc = impl_createDocWithMacro( libName, moduleName, eventHandlerCode );
633         final String documentURL = databaseDoc.getURL();
634 
635         // bind the macro to the OnLoad event
636         final String macroURI = "vnd.sun.star.script:" + libName + "." + moduleName + ".OnLoad?language=Basic&location=document";
637         final XEventsSupplier eventsSupplier = UnoRuntime.queryInterface(XEventsSupplier.class, databaseDoc);
638         eventsSupplier.getEvents().replaceByName("OnLoad", new PropertyValue[]
639                 {
640                     new PropertyValue("EventType", 0, "Script", PropertyState.DIRECT_VALUE),
641                     new PropertyValue("Script", 0, macroURI, PropertyState.DIRECT_VALUE)
642                 });
643 
644         // store the document, and close it
645         impl_storeDocument( databaseDoc );
646         impl_closeDocument( databaseDoc );
647 
648         // ensure the macro security configuration is "ask the user for document macro execution"
649         final int oldSecurityLevel = impl_setMacroSecurityLevel(1);
650 
651         // load it, again
652         m_loadDocState = STATE_LOADING_DOC;
653         // expected order of states is:
654         // STATE_LOADING_DOC - initialized here
655         // STATE_MACRO_EXEC_APPROVED - done in our interaction handler, which auto-approves the execution of macros
656         // STATE_ON_LOAD_RECEIVED - done in our callback for the document events
657         //
658         // In particular, it is important that the interaction handler (which plays the role of the user confirmation
659         // here) is called before the OnLoad notification is received - since the latter happens from within
660         // a Basic macro which is bound to the OnLoad event of the document.
661 
662         final String context = "OnLoad";
663         impl_startObservingEvents(context);
664         databaseDoc = impl_loadDocument( documentURL, impl_getMacroExecLoadArgs() );
665         impl_stopObservingEvents(m_documentEvents, new String[]
666                 {
667                     "OnLoad"
668                 }, context);
669 
670         assertEquals("our provided interaction handler was not called", STATE_ON_LOAD_RECEIVED, m_loadDocState);
671 
672         // restore macro security level
673         impl_setMacroSecurityLevel(oldSecurityLevel);
674 
675         // close the document
676         impl_closeDocument(databaseDoc);
677     }
678 
679     // --------------------------------------------------------------------------------------------------------
680     @Test
681     public void testGlobalEvents() throws Exception, IOException
682     {
683         XModel databaseDoc = impl_createEmptyEmbeddedHSQLDocument();
684         final XStorable storeDoc = UnoRuntime.queryInterface(XStorable.class, databaseDoc);
685 
686         String context, newURL;
687 
688         // XStorable.store
689         final String oldURL = databaseDoc.getURL();
690         context = "store";
691         impl_startObservingEvents(context);
692         storeDoc.store();
693         assertEquals("store is not expected to change the document URL", databaseDoc.getURL(), oldURL);
694         impl_stopObservingEvents(m_globalEvents, new String[]
695                 {
696                     "OnSave", "OnSaveDone"
697                 }, context);
698 
699         // XStorable.storeToURL
700         context = "storeToURL";
701         impl_startObservingEvents(context);
702         storeDoc.storeToURL(createTempFileURL(), new PropertyValue[0]);
703         assertEquals("storetoURL is not expected to change the document URL", databaseDoc.getURL(), oldURL);
704         impl_stopObservingEvents(m_globalEvents, new String[]
705                 {
706                     "OnSaveTo", "OnSaveToDone"
707                 }, context);
708 
709         // XStorable.storeAsURL
710         newURL = createTempFileURL();
711         context = "storeAsURL";
712         impl_startObservingEvents(context);
713         storeDoc.storeAsURL(newURL, new PropertyValue[0]);
714         assertEquals("storeAsURL is expected to change the document URL", databaseDoc.getURL(), newURL);
715         impl_stopObservingEvents(m_globalEvents, new String[]
716                 {
717                     "OnSaveAs", "OnSaveAsDone"
718                 }, context);
719 
720         // XModifiable.setModified
721         final XModifiable modifyDoc = UnoRuntime.queryInterface(XModifiable.class, databaseDoc);
722         context = "setModified";
723         impl_startObservingEvents(context);
724         modifyDoc.setModified(true);
725         assertEquals("setModified didn't work", modifyDoc.isModified(), true);
726         impl_stopObservingEvents(m_globalEvents, new String[]
727                 {
728                     "OnModifyChanged"
729                 }, context);
730 
731         // XStorable.store, with implicit reset of the "Modified" flag
732         context = "store (2)";
733         impl_startObservingEvents(context);
734         storeDoc.store();
735         assertEquals("'store' should implicitly reset the modified flag", modifyDoc.isModified(), false);
736         impl_stopObservingEvents(m_globalEvents, new String[]
737                 {
738                     "OnSave", "OnSaveDone", "OnModifyChanged"
739                 }, context);
740 
741         // XComponentLoader.loadComponentFromURL
742         newURL = copyToTempFile(databaseDoc.getURL());
743         final XComponentLoader loader = UnoRuntime.queryInterface(XComponentLoader.class, getMSF().createInstance("com.sun.star.frame.Desktop"));
744         context = "loadComponentFromURL";
745         impl_startObservingEvents(context);
746         databaseDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(newURL, _BLANK, 0, impl_getDefaultLoadArgs()));
747         impl_stopObservingEvents(m_globalEvents,
748                 new String[]
749                 {
750                     "OnLoadFinished", "OnViewCreated", "OnFocus", "OnLoad"
751                 }, context);
752 
753         // closing a document by API
754         final XCloseable closeDoc = UnoRuntime.queryInterface(XCloseable.class, databaseDoc);
755         context = "close (API)";
756         impl_startObservingEvents(context);
757         closeDoc.close(true);
758         impl_stopObservingEvents(m_globalEvents,
759                 new String[]
760                 {
761                     "OnPrepareUnload", "OnViewClosed", "OnUnload"
762                 }, context);
763 
764         // closing a document via UI
765         context = "close (UI)";
766         impl_startObservingEvents("prepare for '" + context + "'");
767         databaseDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(newURL, _BLANK, 0, impl_getDefaultLoadArgs()));
768         impl_waitForEvent(m_globalEvents, "OnLoad", 5000);
769         // wait for all events to arrive - OnLoad should be the last one
770 
771         final XDispatchProvider dispatchProvider = UnoRuntime.queryInterface(XDispatchProvider.class, databaseDoc.getCurrentController().getFrame());
772         final URL url = impl_getURL(".uno:CloseDoc");
773         final XDispatch dispatcher = dispatchProvider.queryDispatch(url, "", 0);
774         impl_startObservingEvents(context);
775         dispatcher.dispatch(url, new PropertyValue[0]);
776         impl_stopObservingEvents(m_globalEvents,
777                 new String[]
778                 {
779                     "OnPrepareViewClosing", "OnViewClosed", "OnPrepareUnload", "OnUnload"
780                 }, context);
781 
782         // creating a new document
783         databaseDoc = impl_createDocument();
784         final XLoadable loadDoc = UnoRuntime.queryInterface(XLoadable.class, databaseDoc);
785         context = "initNew";
786         impl_startObservingEvents(context);
787         loadDoc.initNew();
788         impl_stopObservingEvents(m_globalEvents, new String[]
789                 {
790                     "OnCreate"
791                 }, context);
792 
793         impl_startObservingEvents(context + " (cleanup)");
794         impl_closeDocument(databaseDoc);
795         impl_waitForEvent(m_globalEvents, "OnUnload", 5000);
796 
797         // focus changes
798         context = "activation";
799         // for this, load a database document ...
800         impl_startObservingEvents("prepare for '" + context + "'");
801         databaseDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(newURL, _BLANK, 0, impl_getDefaultLoadArgs()));
802         final int previousOnLoadEventPos = impl_waitForEvent(m_globalEvents, "OnLoad", 5000);
803         // ... and another document ...
804         final String otherURL = copyToTempFile(databaseDoc.getURL());
805         final XModel otherDoc = UnoRuntime.queryInterface(XModel.class, loader.loadComponentFromURL(otherURL, _BLANK, 0, impl_getDefaultLoadArgs()));
806         impl_raise(otherDoc);
807         impl_waitForEvent(m_globalEvents, "OnLoad", 5000, previousOnLoadEventPos + 1);
808 
809         // ... and switch between the two
810         impl_startObservingEvents(context);
811         impl_raise(databaseDoc);
812         impl_stopObservingEvents(m_globalEvents, new String[]
813                 {
814                     "OnUnfocus", "OnFocus"
815                 }, context);
816 
817         // cleanup
818         impl_startObservingEvents("cleanup after '" + context + "'");
819         impl_closeDocument(databaseDoc);
820         impl_closeDocument(otherDoc);
821     }
822 
823     // --------------------------------------------------------------------------------------------------------
824     private URL impl_getURL(String _completeURL) throws Exception
825     {
826         final URL[] url =
827         {
828             new URL()
829         };
830         url[0].Complete = _completeURL;
831         final XURLTransformer urlTransformer = UnoRuntime.queryInterface(XURLTransformer.class, getMSF().createInstance("com.sun.star.util.URLTransformer"));
832         urlTransformer.parseStrict(url);
833         return url[0];
834     }
835 
836     // --------------------------------------------------------------------------------------------------------
837     private void impl_raise(XModel _document)
838     {
839         final XFrame frame = _document.getCurrentController().getFrame();
840         final XTopWindow topWindow = UnoRuntime.queryInterface(XTopWindow.class, frame.getContainerWindow());
841         topWindow.toFront();
842     }
843 
844     // --------------------------------------------------------------------------------------------------------
845     private void impl_startObservingEvents(String _context)
846     {
847         System.out.println(" " + _context + " {");
848         synchronized (m_documentEvents)
849         {
850             m_documentEvents.clear();
851         }
852         synchronized (m_globalEvents)
853         {
854             m_globalEvents.clear();
855         }
856     }
857 
858     // --------------------------------------------------------------------------------------------------------
859     private void impl_stopObservingEvents(ArrayList _actualEvents, String[] _expectedEvents, String _context)
860     {
861         try
862         {
863             synchronized (_actualEvents)
864             {
865                 int actualEventCount = _actualEvents.size();
866                 while (actualEventCount < _expectedEvents.length)
867                 {
868                     // well, it's possible not all events already arrived, yet - finally, some of them
869                     // are notified asynchronously
870                     // So, wait a few seconds.
871                     try
872                     {
873                         _actualEvents.wait(20000);
874                     }
875                     catch (InterruptedException ex)
876                     {
877                     }
878 
879                     if (actualEventCount == _actualEvents.size())
880                     // the above wait was left because of the timeout, *not* because an event
881                     // arrived. Okay, we won't wait any longer, this is a failure.
882                     {
883                         break;
884                     }
885                     actualEventCount = _actualEvents.size();
886                 }
887 
888                 assertEquals("wrong event count for '" + _context + "'",
889                         _expectedEvents.length, _actualEvents.size());
890 
891                 for (int i = 0; i < _expectedEvents.length; ++i)
892                 {
893                     assertEquals("wrong event at positon " + (i + 1) + " for '" + _context + "'",
894                             _expectedEvents[i], _actualEvents.get(i));
895                 }
896             }
897         }
898         finally
899         {
900             System.out.println(" }");
901         }
902     }
903 
904     // --------------------------------------------------------------------------------------------------------
905     int impl_waitForEvent(ArrayList _eventQueue, String _expectedEvent, int _maxMilliseconds)
906     {
907         return impl_waitForEvent(_eventQueue, _expectedEvent, _maxMilliseconds, 0);
908     }
909 
910     // --------------------------------------------------------------------------------------------------------
911     int impl_waitForEvent(ArrayList _eventQueue, String _expectedEvent, int _maxMilliseconds, int _firstQueueElementToCheck)
912     {
913         synchronized (_eventQueue)
914         {
915             int waitedMilliseconds = 0;
916 
917             while (waitedMilliseconds < _maxMilliseconds)
918             {
919                 for (int i = _firstQueueElementToCheck; i < _eventQueue.size(); ++i)
920                 {
921                     if (_expectedEvent.equals(_eventQueue.get(i)))
922                     // found the event in the queue
923                     {
924                         return i;
925                     }
926                 }
927 
928                 // wait a little, perhaps the event will still arrive
929                 try
930                 {
931                     _eventQueue.wait(500);
932                     waitedMilliseconds += 500;
933                 }
934                 catch (InterruptedException e)
935                 {
936                 }
937             }
938         }
939 
940         fail("expected event '" + _expectedEvent + "' did not arrive after " + _maxMilliseconds + " milliseconds");
941         return -1;
942     }
943 
944     // --------------------------------------------------------------------------------------------------------
945     void onDocumentEvent(DocumentEvent _Event)
946     {
947         if ("OnTitleChanged".equals(_Event.EventName))
948         // OnTitleChanged events are notified too often. This is known, and accepted.
949         // (the deeper reason is that it's diffult to determine, in the DatabaseDocument implementatin,
950         // when the title actually changed. In particular, when we do a saveAsURL, and then ask for a
951         // title *before* the TitleHelper got the document's OnSaveAsDone event, then the wrong (old)
952         // title is obtained.
953         {
954             return;
955         }
956 
957         if ((_Event.EventName.equals("OnLoad")) && (m_loadDocState != STATE_NOT_STARTED))
958         {
959             assertEquals("OnLoad event must come *after* invocation of the interaction handler / user!",
960                     m_loadDocState, STATE_MACRO_EXEC_APPROVED);
961             m_loadDocState = STATE_ON_LOAD_RECEIVED;
962         }
963 
964         synchronized (m_documentEvents)
965         {
966             m_documentEvents.add(_Event.EventName);
967             m_documentEvents.notifyAll();
968         }
969 
970         System.out.println("  document event: " + _Event.EventName);
971     }
972 
973     // --------------------------------------------------------------------------------------------------------
974     public void documentEventOccured(DocumentEvent _Event)
975     {
976         if ("OnTitleChanged".equals(_Event.EventName))
977         // ignore. See onDocumentEvent for a justification
978         {
979             return;
980         }
981 
982         synchronized (m_globalEvents)
983         {
984             m_globalEvents.add(_Event.EventName);
985             m_globalEvents.notifyAll();
986         }
987 
988         System.out.println("  global event: " + _Event.EventName);
989     }
990 
991     // --------------------------------------------------------------------------------------------------------
992     public void disposing(EventObject _Event)
993     {
994         // not interested in
995     }
996 }
997