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