Line data Source code
1 : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : /*
3 : * This file is part of the LibreOffice project.
4 : *
5 : * This Source Code Form is subject to the terms of the Mozilla Public
6 : * License, v. 2.0. If a copy of the MPL was not distributed with this
7 : * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 : */
9 :
10 : #include <sal/config.h>
11 :
12 : #include <set>
13 :
14 : #include <swmodeltestbase.hxx>
15 :
16 : #include <com/sun/star/awt/FontWeight.hpp>
17 : #include <com/sun/star/style/PageStyleLayout.hpp>
18 : #include <com/sun/star/table/XCell.hpp>
19 : #include <com/sun/star/table/BorderLine.hpp>
20 : #include <com/sun/star/text/XTextTable.hpp>
21 : #include <com/sun/star/text/MailMergeType.hpp>
22 : #include <com/sun/star/sdb/XDocumentDataSource.hpp>
23 : #include <com/sun/star/text/TextContentAnchorType.hpp>
24 :
25 : #include <wrtsh.hxx>
26 : #include <ndtxt.hxx>
27 : #include <swdtflvr.hxx>
28 : #include <view.hxx>
29 : #include <edtwin.hxx>
30 : #include <olmenu.hxx>
31 : #include <cmdid.h>
32 :
33 : /**
34 : * Maps database URIs to the registered database names for quick lookups
35 : */
36 : typedef std::map<OUString, OUString> DBuriMap;
37 1 : DBuriMap aDBuriMap;
38 :
39 8 : class MMTest : public SwModelTestBase
40 : {
41 : public:
42 : MMTest();
43 :
44 8 : virtual void tearDown() SAL_OVERRIDE
45 : {
46 8 : if (mxMMComponent.is())
47 : {
48 5 : if (mnCurOutputType == text::MailMergeType::SHELL)
49 : {
50 0 : SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument*>(mxMMComponent.get());
51 0 : CPPUNIT_ASSERT(pTextDoc);
52 0 : pTextDoc->GetDocShell()->DoClose();
53 : }
54 : else
55 5 : mxMMComponent->dispose();
56 : }
57 8 : SwModelTestBase::tearDown();
58 8 : }
59 :
60 : /**
61 : * Helper func used by each unit test to test the 'mail merge' code.
62 : *
63 : * Registers the data source, loads the original file as reference,
64 : * initializes the mail merge job and its default argument sequence.
65 : *
66 : * The 'verify' method actually has to execute the mail merge by
67 : * calling executeMailMerge() after modifying the job arguments.
68 : */
69 8 : void executeMailMergeTest(const char* filename, const char* datasource, const char* tablename, bool file)
70 : {
71 8 : header();
72 8 : preTest(filename);
73 8 : load(mpTestDocumentPath, filename);
74 :
75 8 : utl::TempFile aTempDir(nullptr, true);
76 16 : const OUString aWorkDir = aTempDir.GetURL();
77 16 : const OUString aURI( getURLFromSrc(mpTestDocumentPath) + OUString::createFromAscii(datasource) );
78 16 : OUString aDBName = registerDBsource( aURI, aWorkDir );
79 8 : initMailMergeJobAndArgs( filename, tablename, aDBName, "LOMM_", aWorkDir, file );
80 :
81 8 : postTest(filename);
82 8 : verify();
83 8 : finish();
84 :
85 8 : ::utl::removeTree(aWorkDir);
86 16 : mnCurOutputType = 0;
87 8 : }
88 :
89 8 : OUString registerDBsource( const OUString &aURI, const OUString &aWorkDir )
90 : {
91 8 : OUString aDBName;
92 8 : DBuriMap::const_iterator pos = aDBuriMap.find( aURI );
93 8 : if (pos == aDBuriMap.end())
94 : {
95 3 : aDBName = SwDBManager::LoadAndRegisterDataSource( aURI, NULL, &aWorkDir );
96 3 : aDBuriMap.insert( std::pair< OUString, OUString >( aURI, aDBName ) );
97 3 : std::cout << "New datasource name: '" << aDBName << "'" << std::endl;
98 : }
99 : else
100 : {
101 5 : aDBName = pos->second;
102 5 : std::cout << "Old datasource name: '" << aDBName << "'" << std::endl;
103 : }
104 8 : CPPUNIT_ASSERT(!aDBName.isEmpty());
105 8 : return aDBName;
106 : }
107 :
108 8 : void initMailMergeJobAndArgs( const char* filename, const char* tablename, const OUString &aDBName,
109 : const OUString &aPrefix, const OUString &aWorkDir, bool file )
110 : {
111 8 : uno::Reference< task::XJob > xJob( getMultiServiceFactory()->createInstance( "com.sun.star.text.MailMerge" ), uno::UNO_QUERY_THROW );
112 8 : mxJob.set( xJob );
113 :
114 8 : int seq_id = 5;
115 8 : if (tablename) seq_id += 2;
116 8 : mSeqMailMergeArgs.realloc( seq_id );
117 :
118 8 : seq_id = 0;
119 8 : mSeqMailMergeArgs[ seq_id++ ] = beans::NamedValue( OUString( UNO_NAME_OUTPUT_TYPE ), uno::Any( file ? text::MailMergeType::FILE : text::MailMergeType::SHELL ) );
120 16 : mSeqMailMergeArgs[ seq_id++ ] = beans::NamedValue( OUString( UNO_NAME_DOCUMENT_URL ), uno::Any(
121 24 : ( OUString(getURLFromSrc(mpTestDocumentPath) + OUString::createFromAscii(filename)) ) ) );
122 8 : mSeqMailMergeArgs[ seq_id++ ] = beans::NamedValue( OUString( UNO_NAME_DATA_SOURCE_NAME ), uno::Any( aDBName ) );
123 8 : mSeqMailMergeArgs[ seq_id++ ] = beans::NamedValue( OUString( UNO_NAME_OUTPUT_URL ), uno::Any( aWorkDir ) );
124 8 : mSeqMailMergeArgs[ seq_id++ ] = beans::NamedValue( OUString( UNO_NAME_FILE_NAME_PREFIX ), uno::Any( aPrefix ));
125 8 : if (tablename)
126 : {
127 8 : mSeqMailMergeArgs[ seq_id++ ] = beans::NamedValue( OUString( UNO_NAME_DAD_COMMAND_TYPE ), uno::Any( sdb::CommandType::TABLE ) );
128 8 : mSeqMailMergeArgs[ seq_id++ ] = beans::NamedValue( OUString( UNO_NAME_DAD_COMMAND ), uno::Any( OUString::createFromAscii(tablename) ) );
129 8 : }
130 8 : }
131 :
132 8 : void executeMailMerge()
133 : {
134 8 : uno::Any res = mxJob->execute( mSeqMailMergeArgs );
135 :
136 8 : const beans::NamedValue *pArguments = mSeqMailMergeArgs.getConstArray();
137 8 : bool bOk = true;
138 8 : sal_Int32 nArgs = mSeqMailMergeArgs.getLength();
139 :
140 64 : for (sal_Int32 i = 0; i < nArgs; ++i) {
141 56 : const OUString &rName = pArguments[i].Name;
142 56 : const uno::Any &rValue = pArguments[i].Value;
143 :
144 : // all error checking was already done by the MM job execution
145 56 : if (rName == UNO_NAME_OUTPUT_URL)
146 8 : bOk &= rValue >>= mailMergeOutputURL;
147 48 : else if (rName == UNO_NAME_FILE_NAME_PREFIX)
148 8 : bOk &= rValue >>= mailMergeOutputPrefix;
149 40 : else if (rName == UNO_NAME_OUTPUT_TYPE)
150 8 : bOk &= rValue >>= mnCurOutputType;
151 : }
152 :
153 8 : CPPUNIT_ASSERT(bOk);
154 :
155 8 : if (mnCurOutputType == text::MailMergeType::SHELL)
156 : {
157 5 : CPPUNIT_ASSERT(res >>= mxMMComponent);
158 5 : CPPUNIT_ASSERT(mxMMComponent.is());
159 : }
160 : else
161 : {
162 3 : CPPUNIT_ASSERT(res == true);
163 3 : loadMailMergeDocument( 0 );
164 8 : }
165 8 : }
166 :
167 : /**
168 : * Like parseExport(), but for given mail merge document.
169 : */
170 1 : xmlDocPtr parseMailMergeExport(int number, const OUString& rStreamName = OUString("word/document.xml"))
171 : {
172 1 : if (mnCurOutputType != text::MailMergeType::FILE)
173 0 : return 0;
174 :
175 1 : OUString name = mailMergeOutputPrefix + OUString::number( number ) + ".odt";
176 1 : return parseExportInternal( mailMergeOutputURL + "/" + name, rStreamName );
177 : }
178 :
179 : /**
180 : Loads number-th document from mail merge. Requires file output from mail merge.
181 : */
182 23 : void loadMailMergeDocument( int number )
183 : {
184 : assert( mnCurOutputType == text::MailMergeType::FILE );
185 23 : if (mxComponent.is())
186 23 : mxComponent->dispose();
187 23 : OUString name = mailMergeOutputPrefix + OUString::number( number ) + ".odt";
188 : // Output name early, so in the case of a hang, the name of the hanging input file is visible.
189 23 : std::cout << name << ",";
190 23 : mnStartTime = osl_getGlobalTimer();
191 23 : mxComponent = loadFromDesktop(mailMergeOutputURL + "/" + name, "com.sun.star.text.TextDocument");
192 23 : CPPUNIT_ASSERT( mxComponent.is());
193 46 : OString name2 = OUStringToOString( name, RTL_TEXTENCODING_UTF8 );
194 23 : discardDumpedLayout();
195 23 : if (mustCalcLayoutOf(name2.getStr()))
196 46 : calcLayout();
197 23 : }
198 :
199 : protected:
200 : // Returns page number of the first page of a MM document inside the large MM document (used in the SHELL case).
201 : int documentStartPageNumber( int document ) const;
202 :
203 : uno::Reference< com::sun::star::task::XJob > mxJob;
204 : uno::Sequence< beans::NamedValue > mSeqMailMergeArgs;
205 : OUString mailMergeOutputURL;
206 : OUString mailMergeOutputPrefix;
207 : sal_Int16 mnCurOutputType;
208 : uno::Reference< lang::XComponent > mxMMComponent;
209 : };
210 :
211 : #define DECLARE_MAILMERGE_TEST(TestName, filename, datasource, tablename, file, BaseClass) \
212 : class TestName : public BaseClass { \
213 : protected: \
214 : virtual OUString getTestName() SAL_OVERRIDE { return OUString(#TestName); } \
215 : public: \
216 : CPPUNIT_TEST_SUITE(TestName); \
217 : CPPUNIT_TEST(MailMerge); \
218 : CPPUNIT_TEST_SUITE_END(); \
219 : \
220 : void MailMerge() { \
221 : executeMailMergeTest(filename, datasource, tablename, file); \
222 : } \
223 : void verify() SAL_OVERRIDE; \
224 : }; \
225 : CPPUNIT_TEST_SUITE_REGISTRATION(TestName); \
226 : void TestName::verify()
227 :
228 : // Will generate the resulting document in mxMMDocument.
229 : #define DECLARE_SHELL_MAILMERGE_TEST(TestName, filename, datasource, tablename) \
230 : DECLARE_MAILMERGE_TEST(TestName, filename, datasource, tablename, false, MMTest)
231 :
232 : // Will generate documents as files, use loadMailMergeDocument().
233 : #define DECLARE_FILE_MAILMERGE_TEST(TestName, filename, datasource, tablename) \
234 : DECLARE_MAILMERGE_TEST(TestName, filename, datasource, tablename, true, MMTest)
235 :
236 20 : int MMTest::documentStartPageNumber( int document ) const
237 : { // See SwMailMergeOutputPage::documentStartPageNumber() .
238 20 : SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument *>(mxMMComponent.get());
239 20 : CPPUNIT_ASSERT(pTextDoc);
240 20 : SwWrtShell* shell = pTextDoc->GetDocShell()->GetWrtShell();
241 20 : IDocumentMarkAccess* marks = shell->GetDoc()->getIDocumentMarkAccess();
242 : // Unfortunately, the pages are marked using UNO bookmarks, which have internals names, so they cannot be referred to by their names.
243 : // Assume that there are no other UNO bookmarks than the ones used by mail merge, and that they are in the sorted order.
244 20 : IDocumentMarkAccess::const_iterator_t mark;
245 20 : int pos = 0;
246 110 : for( mark = marks->getAllMarksBegin(); mark != marks->getAllMarksEnd() && pos < document; ++mark )
247 : {
248 90 : if( IDocumentMarkAccess::GetType( **mark ) == IDocumentMarkAccess::MarkType::UNO_BOOKMARK )
249 90 : ++pos;
250 : }
251 20 : CPPUNIT_ASSERT( pos == document );
252 : sal_uInt16 page, dummy;
253 20 : shell->Push();
254 20 : shell->GotoMark( mark->get());
255 20 : shell->GetPageNum( page, dummy );
256 20 : shell->Pop(false);
257 20 : return page;
258 : }
259 8 : MMTest::MMTest()
260 : : SwModelTestBase("/sw/qa/extras/mailmerge/data/", "writer8")
261 8 : , mnCurOutputType(0)
262 : {
263 8 : }
264 :
265 12 : DECLARE_SHELL_MAILMERGE_TEST(testMultiPageAnchoredDraws, "multiple-page-anchored-draws.odt", "4_v01.ods", "Tabelle1")
266 : {
267 1 : executeMailMerge();
268 :
269 1 : SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument *>(mxMMComponent.get());
270 1 : CPPUNIT_ASSERT(pTextDoc);
271 1 : sal_uInt16 nPhysPages = pTextDoc->GetDocShell()->GetWrtShell()->GetPhyPageNum();
272 1 : CPPUNIT_ASSERT_EQUAL(sal_uInt16(8), nPhysPages);
273 :
274 1 : uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxMMComponent, uno::UNO_QUERY);
275 2 : uno::Reference<container::XIndexAccess> xDraws(xDrawPageSupplier->getDrawPage(), uno::UNO_QUERY);
276 1 : CPPUNIT_ASSERT_EQUAL(sal_Int32(8), xDraws->getCount());
277 :
278 2 : std::set<sal_uInt16> pages;
279 2 : uno::Reference<beans::XPropertySet> xPropertySet;
280 :
281 9 : for (sal_Int32 i = 0; i < xDraws->getCount(); i++)
282 : {
283 : text::TextContentAnchorType nAnchorType;
284 : sal_uInt16 nAnchorPageNo;
285 8 : xPropertySet.set(xDraws->getByIndex(i), uno::UNO_QUERY);
286 :
287 8 : xPropertySet->getPropertyValue( UNO_NAME_ANCHOR_TYPE ) >>= nAnchorType;
288 8 : CPPUNIT_ASSERT_EQUAL( text::TextContentAnchorType_AT_PAGE, nAnchorType );
289 :
290 8 : xPropertySet->getPropertyValue( UNO_NAME_ANCHOR_PAGE_NO ) >>= nAnchorPageNo;
291 : // are all shapes are on different page numbers?
292 8 : CPPUNIT_ASSERT(pages.insert(nAnchorPageNo).second);
293 1 : }
294 1 : }
295 :
296 12 : DECLARE_FILE_MAILMERGE_TEST(testMissingDefaultLineColor, "missing-default-line-color.ott", "one-empty-address.ods", "one-empty-address")
297 : {
298 1 : executeMailMerge();
299 : // The document was created by LO version which didn't write out the default value for line color
300 : // (see XMLGraphicsDefaultStyle::SetDefaults()).
301 1 : uno::Reference<drawing::XDrawPageSupplier> xDrawPageSupplier(mxComponent, uno::UNO_QUERY);
302 2 : uno::Reference<container::XIndexAccess> xDraws(xDrawPageSupplier->getDrawPage(), uno::UNO_QUERY);
303 2 : uno::Reference<beans::XPropertySet> xPropertySet(xDraws->getByIndex(0), uno::UNO_QUERY);
304 : // Lines do not have a line color.
305 1 : CPPUNIT_ASSERT( !xPropertySet->getPropertySetInfo()->hasPropertyByName( "LineColor" ));
306 1 : SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument *>(mxComponent.get());
307 1 : CPPUNIT_ASSERT(pTextDoc);
308 2 : uno::Reference< lang::XMultiServiceFactory > xFact( mxComponent, uno::UNO_QUERY );
309 2 : uno::Reference< beans::XPropertySet > xDefaults( xFact->createInstance( "com.sun.star.drawing.Defaults" ), uno::UNO_QUERY );
310 1 : CPPUNIT_ASSERT( xDefaults.is());
311 2 : uno::Reference< beans::XPropertySetInfo > xInfo( xDefaults->getPropertySetInfo());
312 1 : CPPUNIT_ASSERT( xInfo->hasPropertyByName( "LineColor" ));
313 : sal_uInt32 lineColor;
314 1 : xDefaults->getPropertyValue( "LineColor" ) >>= lineColor;
315 : // And the default value is black (wasn't copied properly by mailmerge).
316 1 : CPPUNIT_ASSERT_EQUAL( COL_BLACK, lineColor );
317 : // And check that the resulting file has the proper default.
318 1 : xmlDocPtr pXmlDoc = parseMailMergeExport( 0, "styles.xml" );
319 1 : CPPUNIT_ASSERT_EQUAL( OUString( "graphic" ), getXPath(pXmlDoc, "/office:document-styles/office:styles/style:default-style[1]", "family"));
320 2 : CPPUNIT_ASSERT_EQUAL( OUString( "#000000" ), getXPath(pXmlDoc, "/office:document-styles/office:styles/style:default-style[1]/style:graphic-properties", "stroke-color"));
321 1 : }
322 :
323 12 : DECLARE_FILE_MAILMERGE_TEST(testSimpleMailMerge, "simple-mail-merge.odt", "10-testing-addresses.ods", "testing-addresses")
324 : {
325 1 : executeMailMerge();
326 11 : for( int doc = 0;
327 : doc < 10;
328 : ++doc )
329 : {
330 10 : loadMailMergeDocument( doc );
331 10 : CPPUNIT_ASSERT_EQUAL( 1, getPages());
332 10 : CPPUNIT_ASSERT_EQUAL( OUString( "Fixed text." ), getRun( getParagraph( 1 ), 1 )->getString());
333 10 : CPPUNIT_ASSERT_EQUAL( OUString( "lastname" + OUString::number( doc + 1 )), getRun( getParagraph( 2 ), 1 )->getString());
334 10 : CPPUNIT_ASSERT_EQUAL( OUString( "Another fixed text." ), getRun( getParagraph( 3 ), 1 )->getString());
335 : }
336 1 : }
337 :
338 12 : DECLARE_FILE_MAILMERGE_TEST(test2Pages, "simple-mail-merge-2pages.odt", "10-testing-addresses.ods", "testing-addresses")
339 : {
340 1 : executeMailMerge();
341 11 : for( int doc = 0;
342 : doc < 10;
343 : ++doc )
344 : {
345 10 : loadMailMergeDocument( doc );
346 10 : OUString lastname = "lastname" + OUString::number( doc + 1 );
347 20 : OUString firstname = "firstname" + OUString::number( doc + 1 );
348 10 : CPPUNIT_ASSERT_EQUAL( 2, getPages());
349 10 : CPPUNIT_ASSERT_EQUAL( OUString( "Fixed text." ), getRun( getParagraph( 1 ), 1 )->getString());
350 10 : CPPUNIT_ASSERT_EQUAL( lastname, getRun( getParagraph( 2 ), 1 )->getString());
351 10 : CPPUNIT_ASSERT_EQUAL( OUString( "Another fixed text." ), getRun( getParagraph( 3 ), 1 )->getString());
352 10 : CPPUNIT_ASSERT_EQUAL( OUString( "" ), getRun( getParagraph( 4 ), 1 )->getString()); // empty para at the end of page 1
353 10 : CPPUNIT_ASSERT_EQUAL( OUString( "Second page." ), getRun( getParagraph( 5 ), 1 )->getString());
354 10 : CPPUNIT_ASSERT_EQUAL( firstname, getRun( getParagraph( 6 ), 1 )->getString());
355 : // Also verify the layout.
356 10 : CPPUNIT_ASSERT_EQUAL( lastname, parseDump("/root/page[1]/body/txt[2]/Special", "rText"));
357 10 : CPPUNIT_ASSERT_EQUAL( OUString( "Fixed text." ), parseDump("/root/page[1]/body/txt[1]", ""));
358 10 : CPPUNIT_ASSERT_EQUAL( OUString( "" ), parseDump("/root/page[1]/body/txt[4]", ""));
359 10 : CPPUNIT_ASSERT_EQUAL( OUString( "Second page." ), parseDump("/root/page[2]/body/txt[1]", ""));
360 10 : CPPUNIT_ASSERT_EQUAL( firstname, parseDump("/root/page[2]/body/txt[2]/Special", "rText"));
361 10 : }
362 1 : }
363 :
364 12 : DECLARE_SHELL_MAILMERGE_TEST(testPageBoundariesSimpleMailMerge, "simple-mail-merge.odt", "10-testing-addresses.ods", "testing-addresses")
365 : {
366 : // This is like the test above, but this one uses the create-single-document-containing-everything-generated approach,
367 : // and verifies that boundaries of the generated sub-documents are correct inside that document.
368 : // These boundaries are done using "SwMailMergeOutputPage::documentStartPageNumber<number>" UNO bookmarks (see also
369 : // SwMailMergeOutputPage::documentStartPageNumber() ).
370 1 : executeMailMerge();
371 : // Here getPages() works on the source document, so get pages of the resulting one.
372 1 : SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument *>(mxMMComponent.get());
373 1 : CPPUNIT_ASSERT(pTextDoc);
374 1 : CPPUNIT_ASSERT_EQUAL( sal_uInt16( 19 ), pTextDoc->GetDocShell()->GetWrtShell()->GetPhyPageNum()); // 10 pages, but each sub-document starts on odd page number
375 11 : for( int doc = 0;
376 : doc < 10;
377 : ++doc )
378 : {
379 10 : CPPUNIT_ASSERT_EQUAL( doc * 2 + 1, documentStartPageNumber( doc ));
380 : }
381 1 : }
382 :
383 12 : DECLARE_SHELL_MAILMERGE_TEST(testPageBoundaries2Pages, "simple-mail-merge-2pages.odt", "10-testing-addresses.ods", "testing-addresses")
384 : {
385 1 : executeMailMerge();
386 1 : SwXTextDocument* pTextDoc = dynamic_cast<SwXTextDocument *>(mxMMComponent.get());
387 1 : CPPUNIT_ASSERT(pTextDoc);
388 1 : CPPUNIT_ASSERT_EQUAL( sal_uInt16( 20 ), pTextDoc->GetDocShell()->GetWrtShell()->GetPhyPageNum()); // 20 pages, each sub-document starts on odd page number
389 11 : for( int doc = 0;
390 : doc < 10;
391 : ++doc )
392 : {
393 10 : CPPUNIT_ASSERT_EQUAL( doc * 2 + 1, documentStartPageNumber( doc ));
394 : }
395 1 : }
396 :
397 12 : DECLARE_SHELL_MAILMERGE_TEST(testTdf89214, "tdf89214.odt", "10-testing-addresses.ods", "testing-addresses")
398 : {
399 1 : executeMailMerge();
400 :
401 1 : uno::Reference<text::XTextDocument> xTextDocument(mxMMComponent, uno::UNO_QUERY);
402 2 : uno::Reference<text::XTextRange> xParagraph(getParagraphOrTable(3, xTextDocument->getText()), uno::UNO_QUERY);
403 : // Make sure that we assert the right paragraph.
404 1 : CPPUNIT_ASSERT_EQUAL(OUString("a"), xParagraph->getString());
405 : // This paragraph had a bullet numbering, make sure that the list id is not empty.
406 2 : CPPUNIT_ASSERT(!getProperty<OUString>(xParagraph, "ListId").isEmpty());
407 1 : }
408 :
409 12 : DECLARE_SHELL_MAILMERGE_TEST(testTdf90230, "empty.odt", "10-testing-addresses.ods", "testing-addresses")
410 : {
411 : // MM of an empty document caused an assertion in the SwIndexReg dtor.
412 1 : executeMailMerge();
413 1 : }
414 :
415 4 : CPPUNIT_PLUGIN_IMPLEMENT();
416 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|