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 * This file incorporates work covered by the following license notice:
10 *
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
18 */
19
20 #include <config_features.h>
21
22 #include <vcl/errinf.hxx>
23 #include <tools/urlobj.hxx>
24 #include <svl/converter.hxx>
25 #include <svl/numformat.hxx>
26 #include <comphelper/configuration.hxx>
27 #include <comphelper/processfactory.hxx>
28 #include <comphelper/propertysequence.hxx>
29 #include <comphelper/types.hxx>
30 #include <ucbhelper/content.hxx>
31 #include <svx/txenctab.hxx>
32 #include <unotools/sharedunocomponent.hxx>
33 #include <unotools/charclass.hxx>
34 #include <rtl/character.hxx>
35 #include <rtl/tencinfo.h>
36 #include <sal/log.hxx>
37 #include <osl/diagnose.h>
38 #include <comphelper/diagnose_ex.hxx>
39 #include <o3tl/string_view.hxx>
40
41 #include <com/sun/star/sdb/CommandType.hpp>
42 #include <com/sun/star/sdbc/DataType.hpp>
43 #include <com/sun/star/sdbc/SQLException.hpp>
44 #include <com/sun/star/sdbc/XConnection.hpp>
45 #include <com/sun/star/sdbc/DriverManager.hpp>
46 #include <com/sun/star/sdbc/XResultSetUpdate.hpp>
47 #include <com/sun/star/sdbc/XRow.hpp>
48 #include <com/sun/star/sdbc/XRowSet.hpp>
49 #include <com/sun/star/sdbc/XRowUpdate.hpp>
50 #include <com/sun/star/sdbc/XResultSetMetaDataSupplier.hpp>
51 #include <com/sun/star/sdbcx/XAppend.hpp>
52 #include <com/sun/star/sdbcx/XColumnsSupplier.hpp>
53 #include <com/sun/star/sdbcx/XDataDefinitionSupplier.hpp>
54 #include <com/sun/star/sdbcx/XDataDescriptorFactory.hpp>
55 #include <com/sun/star/sdbcx/XTablesSupplier.hpp>
56 #include <com/sun/star/lang/XMultiServiceFactory.hpp>
57 #include <com/sun/star/beans/XPropertySet.hpp>
58 #include <com/sun/star/ucb/NameClash.hpp>
59 #include <com/sun/star/ucb/TransferInfo.hpp>
60 #include <com/sun/star/ucb/XCommandInfo.hpp>
61
62 #include <scerrors.hxx>
63 #include <docsh.hxx>
64 #include <progress.hxx>
65 #include <editutil.hxx>
66 #include <cellform.hxx>
67 #include <dbdocutl.hxx>
68 #include <dociter.hxx>
69 #include <globstr.hrc>
70 #include <scresid.hxx>
71 #include <svl/zformat.hxx>
72 #include <svl/intitem.hxx>
73 #include <patattr.hxx>
74 #include <scitems.hxx>
75 #include <docpool.hxx>
76 #include <segmenttree.hxx>
77 #include <docparam.hxx>
78 #include <cellvalue.hxx>
79
80 #include <unordered_set>
81 #include <vector>
82
83 using namespace com::sun::star;
84
85 #if HAVE_FEATURE_DBCONNECTIVITY
86
87 constexpr OUString SC_SERVICE_ROWSET = u"com.sun.star.sdb.RowSet"_ustr;
88
89 //! move to a header file?
90 constexpr OUString SC_DBPROP_ACTIVECONNECTION = u"ActiveConnection"_ustr;
91 constexpr OUString SC_DBPROP_COMMAND = u"Command"_ustr;
92 constexpr OUString SC_DBPROP_COMMANDTYPE = u"CommandType"_ustr;
93 constexpr OUStringLiteral SC_DBPROP_PROPCHANGE_NOTIFY = u"PropertyChangeNotificationEnabled";
94
95 constexpr OUString SC_DBPROP_NAME = u"Name"_ustr;
96 constexpr OUStringLiteral SC_DBPROP_TYPE = u"Type";
97 constexpr OUStringLiteral SC_DBPROP_PRECISION = u"Precision";
98 constexpr OUStringLiteral SC_DBPROP_SCALE = u"Scale";
99
100 constexpr OUString SC_DBPROP_EXTENSION = u"Extension"_ustr;
101 constexpr OUString SC_DBPROP_CHARSET = u"CharSet"_ustr;
102
103 namespace
104 {
lcl_getDBaseConnection(uno::Reference<sdbc::XDriverManager2> & _rDrvMgr,uno::Reference<sdbc::XConnection> & _rConnection,OUString & _rTabName,std::u16string_view rFullFileName,rtl_TextEncoding eCharSet)105 ErrCode lcl_getDBaseConnection(uno::Reference<sdbc::XDriverManager2>& _rDrvMgr, uno::Reference<sdbc::XConnection>& _rConnection, OUString& _rTabName, std::u16string_view rFullFileName, rtl_TextEncoding eCharSet)
106 {
107 INetURLObject aURL;
108 aURL.SetSmartProtocol( INetProtocol::File );
109 aURL.SetSmartURL( rFullFileName );
110 _rTabName = aURL.getBase( INetURLObject::LAST_SEGMENT, true,
111 INetURLObject::DecodeMechanism::Unambiguous );
112 OUString aExtension = aURL.getExtension();
113 aURL.removeSegment();
114 aURL.removeFinalSlash();
115 OUString aPath = aURL.GetMainURL(INetURLObject::DecodeMechanism::NONE);
116 const uno::Reference<uno::XComponentContext>& xContext = comphelper::getProcessComponentContext();
117
118 _rDrvMgr.set( sdbc::DriverManager::create( xContext ) );
119
120 // get connection
121
122 const OUString aConnUrl{"sdbc:dbase:" + aPath};
123
124 // sdbc:dbase is based on the css.sdbc.FILEConnectionProperties UNOIDL service, so we can
125 // transport the raw rtl_TextEncoding value instead of having to translate it into an IANA
126 // character set name string (which might not exist for certain eCharSet values, like
127 // RTL_TEXTENCODING_MS_950):
128 uno::Sequence<beans::PropertyValue> aProps( comphelper::InitPropertySequence({
129 { SC_DBPROP_EXTENSION, uno::Any(aExtension) },
130 { SC_DBPROP_CHARSET, uno::Any(eCharSet) }
131 }));
132
133 _rConnection = _rDrvMgr->getConnectionWithInfo( aConnUrl, aProps );
134 return ERRCODE_NONE;
135 }
136 }
137
138 #endif // HAVE_FEATURE_DBCONNECTIVITY
139
140 // MoveFile/KillFile/IsDocument: similar to SfxContentHelper
141
MoveFile(const INetURLObject & rSourceObj,const INetURLObject & rDestObj)142 bool ScDocShell::MoveFile( const INetURLObject& rSourceObj, const INetURLObject& rDestObj )
143 {
144 bool bMoveData = true;
145 bool bRet = true, bKillSource = false;
146 if ( rSourceObj.GetProtocol() != rDestObj.GetProtocol() )
147 {
148 bMoveData = false;
149 bKillSource = true;
150 }
151 OUString aName = rDestObj.getName();
152 INetURLObject aDestPathObj = rDestObj;
153 aDestPathObj.removeSegment();
154 aDestPathObj.setFinalSlash();
155
156 try
157 {
158 ::ucbhelper::Content aDestPath( aDestPathObj.GetMainURL(INetURLObject::DecodeMechanism::NONE),
159 uno::Reference< css::ucb::XCommandEnvironment >(),
160 comphelper::getProcessComponentContext() );
161 uno::Reference< css::ucb::XCommandInfo > xInfo = aDestPath.getCommands();
162 OUString aTransferName = u"transfer"_ustr;
163 if ( xInfo->hasCommandByName( aTransferName ) )
164 {
165 aDestPath.executeCommand( aTransferName, uno::Any(
166 css::ucb::TransferInfo( bMoveData, rSourceObj.GetMainURL(INetURLObject::DecodeMechanism::NONE), aName,
167 css::ucb::NameClash::ERROR ) ) );
168 }
169 else
170 {
171 OSL_FAIL( "transfer command not available" );
172 }
173 }
174 catch( uno::Exception& )
175 {
176 // ucb may throw different exceptions on failure now
177 bRet = false;
178 }
179
180 if ( bKillSource )
181 KillFile( rSourceObj );
182
183 return bRet;
184 }
185
KillFile(const INetURLObject & rURL)186 bool ScDocShell::KillFile( const INetURLObject& rURL )
187 {
188 bool bRet = true;
189 try
190 {
191 ::ucbhelper::Content aCnt( rURL.GetMainURL(INetURLObject::DecodeMechanism::NONE),
192 uno::Reference< css::ucb::XCommandEnvironment >(),
193 comphelper::getProcessComponentContext() );
194 aCnt.executeCommand( u"delete"_ustr, css::uno::Any( true ) );
195 }
196 catch( uno::Exception& )
197 {
198 // ucb may throw different exceptions on failure now
199 bRet = false;
200 }
201
202 return bRet;
203 }
204
IsDocument(const INetURLObject & rURL)205 bool ScDocShell::IsDocument( const INetURLObject& rURL )
206 {
207 bool bRet = false;
208 try
209 {
210 ::ucbhelper::Content aCnt( rURL.GetMainURL(INetURLObject::DecodeMechanism::NONE),
211 uno::Reference< css::ucb::XCommandEnvironment >(),
212 comphelper::getProcessComponentContext() );
213 bRet = aCnt.isDocument();
214 }
215 catch( uno::Exception& )
216 {
217 // ucb may throw different exceptions on failure now - warning only
218 TOOLS_WARN_EXCEPTION( "sc", "Any other exception" );
219 }
220
221 return bRet;
222 }
223
224 #if HAVE_FEATURE_DBCONNECTIVITY
225
lcl_setScalesToColumns(ScDocument & rDoc,const std::vector<tools::Long> & rScales)226 static void lcl_setScalesToColumns(ScDocument& rDoc, const std::vector<tools::Long>& rScales)
227 {
228 SvNumberFormatter* pFormatter = rDoc.GetFormatTable();
229 if (!pFormatter)
230 return;
231
232 SCCOL nColCount = static_cast<SCCOL>(rScales.size());
233 for (SCCOL i = 0; i < nColCount; ++i)
234 {
235 if (rScales[i] < 0)
236 continue;
237
238 sal_uInt32 nOldFormat = rDoc.GetNumberFormat(i, 0, 0);
239 const SvNumberformat* pOldEntry = pFormatter->GetEntry(nOldFormat);
240 if (!pOldEntry)
241 continue;
242
243 LanguageType eLang = pOldEntry->GetLanguage();
244 bool bThousand, bNegRed;
245 sal_uInt16 nPrecision, nLeading;
246 pOldEntry->GetFormatSpecialInfo(bThousand, bNegRed, nPrecision, nLeading);
247
248 nPrecision = static_cast<sal_uInt16>(rScales[i]);
249 OUString aNewPicture = pFormatter->GenerateFormat(nOldFormat, eLang,
250 bThousand, bNegRed, nPrecision, nLeading);
251
252 sal_uInt32 nNewFormat = pFormatter->GetEntryKey(aNewPicture, eLang);
253 if (nNewFormat == NUMBERFORMAT_ENTRY_NOT_FOUND)
254 {
255 sal_Int32 nErrPos = 0;
256 SvNumFormatType nNewType = SvNumFormatType::ALL;
257 bool bOk = pFormatter->PutEntry(
258 aNewPicture, nErrPos, nNewType, nNewFormat, eLang);
259
260 if (!bOk)
261 continue;
262 }
263
264 ScPatternAttr aNewAttrs(rDoc.getCellAttributeHelper());
265 aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nNewFormat));
266 rDoc.ApplyPatternAreaTab(i, 0, i, rDoc.MaxRow(), 0, aNewAttrs);
267 }
268 }
269
270 #endif // HAVE_FEATURE_DBCONNECTIVITY
271
DBaseImport(const OUString & rFullFileName,rtl_TextEncoding eCharSet,std::map<SCCOL,ScColWidthParam> & aColWidthParam,ScFlatBoolRowSegments & rRowHeightsRecalc)272 ErrCode ScDocShell::DBaseImport( const OUString& rFullFileName, rtl_TextEncoding eCharSet,
273 std::map<SCCOL, ScColWidthParam>& aColWidthParam, ScFlatBoolRowSegments& rRowHeightsRecalc )
274 {
275 #if !HAVE_FEATURE_DBCONNECTIVITY
276 (void) rFullFileName;
277 (void) eCharSet;
278 (void) aColWidthParam;
279 (void) rRowHeightsRecalc;
280
281 return ERRCODE_IO_GENERAL;
282 #else
283
284 ErrCode nErr = ERRCODE_NONE;
285
286 try
287 {
288 sal_Int32 nColCount = 0;
289 OUString aTabName;
290 uno::Reference<sdbc::XDriverManager2> xDrvMan;
291 uno::Reference<sdbc::XConnection> xConnection;
292 ErrCode nRet = lcl_getDBaseConnection(xDrvMan,xConnection,aTabName,rFullFileName,eCharSet);
293 if ( !xConnection.is() || !xDrvMan.is() )
294 return nRet;
295 ::utl::DisposableComponent aConnectionHelper(xConnection);
296
297 ScProgress aProgress( this, ScResId( STR_LOAD_DOC ), 0, true );
298 uno::Reference<lang::XMultiServiceFactory> xFactory = comphelper::getProcessServiceFactory();
299 uno::Reference<sdbc::XRowSet> xRowSet( xFactory->createInstance(SC_SERVICE_ROWSET),
300 uno::UNO_QUERY);
301 ::utl::DisposableComponent aRowSetHelper(xRowSet);
302 uno::Reference<beans::XPropertySet> xRowProp( xRowSet, uno::UNO_QUERY );
303 OSL_ENSURE( xRowProp.is(), "can't get RowSet" );
304 if (!xRowProp.is()) return SCERR_IMPORT_CONNECT;
305
306 xRowProp->setPropertyValue( SC_DBPROP_ACTIVECONNECTION, uno::Any(xConnection) );
307
308 xRowProp->setPropertyValue( SC_DBPROP_COMMANDTYPE, uno::Any(sdb::CommandType::TABLE) );
309
310 xRowProp->setPropertyValue( SC_DBPROP_COMMAND, uno::Any(aTabName) );
311
312 xRowProp->setPropertyValue( SC_DBPROP_PROPCHANGE_NOTIFY, uno::Any(false) );
313
314 xRowSet->execute();
315
316 uno::Reference<sdbc::XResultSetMetaData> xMeta;
317 uno::Reference<sdbc::XResultSetMetaDataSupplier> xMetaSupp( xRowSet, uno::UNO_QUERY );
318 if ( xMetaSupp.is() )
319 xMeta = xMetaSupp->getMetaData();
320 if ( xMeta.is() )
321 nColCount = xMeta->getColumnCount(); // this is the number of real columns
322
323 if ( nColCount > m_pDocument->MaxCol()+1 )
324 {
325 nColCount = m_pDocument->MaxCol()+1;
326 nErr = SCWARN_IMPORT_COLUMN_OVERFLOW; // warning
327 }
328
329 uno::Reference<sdbc::XRow> xRow( xRowSet, uno::UNO_QUERY );
330 OSL_ENSURE( xRow.is(), "can't get Row" );
331 if (!xRow.is()) return SCERR_IMPORT_CONNECT;
332
333 // currency flag is not needed for dBase
334 uno::Sequence<sal_Int32> aColTypes( nColCount ); // column types
335 sal_Int32* pTypeArr = aColTypes.getArray();
336 for (sal_Int32 i=0; i<nColCount; i++)
337 pTypeArr[i] = xMeta->getColumnType( i+1 );
338
339 // read column names
340 //! add type descriptions
341
342 aProgress.SetState( 0 );
343
344 std::vector<tools::Long> aScales(nColCount, -1);
345 for (sal_Int32 i=0; i<nColCount; i++)
346 {
347 OUString aHeader = xMeta->getColumnLabel( i+1 );
348
349 switch ( pTypeArr[i] )
350 {
351 case sdbc::DataType::BIT:
352 aHeader += ",L";
353 break;
354 case sdbc::DataType::DATE:
355 aHeader += ",D";
356 break;
357 case sdbc::DataType::LONGVARCHAR:
358 aHeader += ",M";
359 break;
360 case sdbc::DataType::VARCHAR:
361 aHeader += ",C," + OUString::number( xMeta->getColumnDisplaySize( i+1 ) );
362 break;
363 case sdbc::DataType::DECIMAL:
364 {
365 tools::Long nPrec = xMeta->getPrecision( i+1 );
366 tools::Long nScale = xMeta->getScale( i+1 );
367 aHeader += ",N," +
368 OUString::number(
369 SvDbaseConverter::ConvertPrecisionToDbase(
370 nPrec, nScale ) ) +
371 "," +
372 OUString::number( nScale );
373 aScales[i] = nScale;
374 }
375 break;
376 }
377
378 m_pDocument->SetString( static_cast<SCCOL>(i), 0, 0, aHeader );
379 }
380
381 lcl_setScalesToColumns(*m_pDocument, aScales);
382
383 SCROW nRow = 1; // 0 is column titles
384 bool bEnd = false;
385 size_t nErrors(0);
386 while ( !bEnd && xRowSet->next() )
387 {
388 if (nRow <= m_pDocument->MaxRow())
389 {
390 bool bSimpleRow = true;
391 SCCOL nCol = 0;
392 for (sal_Int32 i=0; i<nColCount; i++)
393 {
394 ScDatabaseDocUtil::StrData aStrData;
395 bool bWasError =
396 ScDatabaseDocUtil::PutData( *m_pDocument, nCol, nRow, 0,
397 xRow, i+1, pTypeArr[i], false,
398 &aStrData );
399
400 if (bWasError)
401 ++nErrors;
402
403 if (aStrData.mnStrLength > aColWidthParam[nCol].mnMaxTextLen)
404 {
405 aColWidthParam[nCol].mnMaxTextLen = aStrData.mnStrLength;
406 aColWidthParam[nCol].mnMaxTextRow = nRow;
407 }
408
409 if (!aStrData.mbSimpleText)
410 {
411 bSimpleRow = false;
412 aColWidthParam[nCol].mbSimpleText = false;
413 }
414
415 ++nCol;
416 }
417 if (nErrors > 65535 && comphelper::IsFuzzing())
418 {
419 bEnd = true;
420 nErr = ERRCODE_IO_GENERAL;
421 SAL_WARN("sc", "Too many errors: abandoning.");
422 }
423 if (!bSimpleRow)
424 rRowHeightsRecalc.setTrue(nRow, nRow);
425 ++nRow;
426 }
427 else // past the end of the spreadsheet
428 {
429 bEnd = true; // don't continue
430 nErr = SCWARN_IMPORT_RANGE_OVERFLOW; // warning message
431 }
432 }
433 }
434 catch ( sdbc::SQLException& )
435 {
436 nErr = SCERR_IMPORT_CONNECT;
437 }
438 catch ( uno::Exception& )
439 {
440 TOOLS_WARN_EXCEPTION( "sc", "Unexpected exception in database");
441 nErr = ERRCODE_IO_GENERAL;
442 }
443
444 return nErr;
445 #endif // HAVE_FEATURE_DBCONNECTIVITY
446 }
447
448 #if HAVE_FEATURE_DBCONNECTIVITY
449
450 namespace {
451
lcl_GetColumnTypes(ScDocShell & rDocShell,const ScRange & rDataRange,bool bHasFieldNames,OUString * pColNames,sal_Int32 * pColTypes,sal_Int32 * pColLengths,sal_Int32 * pColScales,bool & bHasMemo,rtl_TextEncoding eCharSet)452 void lcl_GetColumnTypes(
453 ScDocShell& rDocShell, const ScRange& rDataRange, bool bHasFieldNames,
454 OUString* pColNames, sal_Int32* pColTypes, sal_Int32* pColLengths,
455 sal_Int32* pColScales, bool& bHasMemo, rtl_TextEncoding eCharSet )
456 {
457 ScDocument& rDoc = rDocShell.GetDocument();
458 ScInterpreterContext& rContext = rDoc.GetNonThreadedContext();
459
460 SCTAB nTab = rDataRange.aStart.Tab();
461 SCCOL nFirstCol = rDataRange.aStart.Col();
462 SCROW nFirstRow = rDataRange.aStart.Row();
463 SCCOL nLastCol = rDataRange.aEnd.Col();
464 SCROW nLastRow = rDataRange.aEnd.Row();
465
466 typedef std::unordered_set<OUString> StrSetType;
467 StrSetType aFieldNames;
468
469 tools::Long nField = 0;
470 SCROW nFirstDataRow = ( bHasFieldNames ? nFirstRow + 1 : nFirstRow );
471 for ( SCCOL nCol = nFirstCol; nCol <= nLastCol; nCol++ )
472 {
473 bool bTypeDefined = false;
474 bool bPrecDefined = false;
475 sal_Int32 nFieldLen = 0;
476 sal_Int32 nPrecision = 0;
477 sal_Int32 nDbType = sdbc::DataType::SQLNULL;
478 OUString aFieldName;
479
480 // Fieldname[,Type[,Width[,Prec]]]
481 // Type etc.: L; D; C[,W]; N[,W[,P]]
482 if ( bHasFieldNames )
483 {
484 OUString aString {rDoc.GetString(nCol, nFirstRow, nTab).toAsciiUpperCase()};
485 sal_Int32 nIdx {0};
486 aFieldName = aString.getToken( 0, ',', nIdx);
487 if ( nIdx>0 )
488 {
489 aString = aString.replaceAll(" ", "");
490 switch ( o3tl::getToken(aString, 0, ',', nIdx )[0] )
491 {
492 case 'L' :
493 nDbType = sdbc::DataType::BIT;
494 nFieldLen = 1;
495 bTypeDefined = true;
496 bPrecDefined = true;
497 break;
498 case 'D' :
499 nDbType = sdbc::DataType::DATE;
500 nFieldLen = 8;
501 bTypeDefined = true;
502 bPrecDefined = true;
503 break;
504 case 'M' :
505 nDbType = sdbc::DataType::LONGVARCHAR;
506 nFieldLen = 10;
507 bTypeDefined = true;
508 bPrecDefined = true;
509 bHasMemo = true;
510 break;
511 case 'C' :
512 nDbType = sdbc::DataType::VARCHAR;
513 bTypeDefined = true;
514 bPrecDefined = true;
515 break;
516 case 'N' :
517 nDbType = sdbc::DataType::DECIMAL;
518 bTypeDefined = true;
519 break;
520 }
521 if ( bTypeDefined && !nFieldLen && nIdx>0 )
522 {
523 nFieldLen = o3tl::toInt32(o3tl::getToken(aString, 0, ',', nIdx ));
524 if ( !bPrecDefined && nIdx>0 )
525 {
526 OUString aTmp( aString.getToken( 0, ',', nIdx ) );
527 if ( CharClass::isAsciiNumeric(aTmp) )
528 {
529 nPrecision = aTmp.toInt32();
530 if (nPrecision && nFieldLen < nPrecision+1)
531 nFieldLen = nPrecision + 1; // include decimal separator
532 bPrecDefined = true;
533 }
534 }
535 }
536 }
537
538 // Check field name and generate valid field name if necessary.
539 // First character has to be alphabetical, subsequent characters
540 // have to be alphanumerical or underscore.
541 // "_DBASELOCK" is reserved (obsolete because first character is
542 // not alphabetical).
543 // No duplicated names.
544 if ( !rtl::isAsciiAlpha(aFieldName[0]) )
545 aFieldName = "N" + aFieldName;
546 OUStringBuffer aTmpStr;
547 sal_Unicode c;
548 for ( const sal_Unicode* p = aFieldName.getStr(); ( c = *p ) != 0; p++ )
549 {
550 if ( rtl::isAsciiAlpha(c) || rtl::isAsciiDigit(c) || c == '_' )
551 aTmpStr.append(c);
552 else
553 aTmpStr.append("_");
554 }
555 aFieldName = aTmpStr.makeStringAndClear();
556 if ( aFieldName.getLength() > 10 )
557 aFieldName = aFieldName.copy(0, 10);
558
559 if (!aFieldNames.insert(aFieldName).second)
560 { // Duplicated field name, append numeric suffix.
561 sal_uInt16 nSub = 1;
562 OUString aFixPart( aFieldName );
563 do
564 {
565 ++nSub;
566 OUString aVarPart = OUString::number( nSub );
567 if ( aFixPart.getLength() + aVarPart.getLength() > 10 )
568 aFixPart = aFixPart.copy( 0, 10 - aVarPart.getLength() );
569 aFieldName = aFixPart + aVarPart;
570 } while (!aFieldNames.insert(aFieldName).second);
571 }
572 }
573 else
574 {
575 aFieldName = "N" + OUString::number(nCol+1);
576 }
577
578 if ( !bTypeDefined )
579 { // Field type.
580 ScRefCellValue aCell(rDoc, ScAddress(nCol, nFirstDataRow, nTab));
581 if (aCell.isEmpty() || aCell.hasString())
582 nDbType = sdbc::DataType::VARCHAR;
583 else
584 {
585 sal_uInt32 nFormat = rDoc.GetNumberFormat( nCol, nFirstDataRow, nTab );
586 switch (rContext.NFGetType(nFormat))
587 {
588 case SvNumFormatType::LOGICAL :
589 nDbType = sdbc::DataType::BIT;
590 nFieldLen = 1;
591 break;
592 case SvNumFormatType::DATE :
593 nDbType = sdbc::DataType::DATE;
594 nFieldLen = 8;
595 break;
596 case SvNumFormatType::TIME :
597 case SvNumFormatType::DATETIME :
598 nDbType = sdbc::DataType::VARCHAR;
599 break;
600 default:
601 nDbType = sdbc::DataType::DECIMAL;
602 }
603 }
604 }
605 // Field length.
606 if ( nDbType == sdbc::DataType::VARCHAR && !nFieldLen )
607 { // Determine maximum field width.
608 nFieldLen = rDoc.GetMaxStringLen( nTab, nCol, nFirstDataRow,
609 nLastRow, eCharSet );
610 if ( nFieldLen == 0 )
611 nFieldLen = 1;
612 }
613 else if ( nDbType == sdbc::DataType::DECIMAL )
614 { // Determine maximum field width and precision.
615 sal_Int32 nLen;
616 sal_uInt16 nPrec;
617 nLen = rDoc.GetMaxNumberStringLen( nPrec, nTab, nCol,
618 nFirstDataRow, nLastRow );
619 // dBaseIII precision limit: 15
620 if ( nPrecision > 15 )
621 nPrecision = 15;
622 if ( nPrec > 15 )
623 nPrec = 15;
624 if ( bPrecDefined && nPrecision != nPrec )
625 {
626 if (nPrecision < nPrec)
627 {
628 // This is a hairy case. User defined nPrecision but a
629 // number format has more precision. Modifying a dBase
630 // field may as well render the resulting file useless for
631 // an application that relies on its defined structure,
632 // especially if we are resaving an already existing file.
633 // So who's right, the user who (or the loaded file that)
634 // defined the field, or the user who applied the format?
635 // Commit f59e350d1733125055f1144f8b3b1b0a46f6d1ca gave the
636 // format a higher priority, which is debatable.
637 SAL_WARN( "sc", "lcl_GetColumnTypes: conflicting dBase field precision for "
638 << aFieldName << " (" << nPrecision << "<" << nPrec << ")");
639
640 // Adjust length to larger predefined integer part. There
641 // may be a reason that the field was prepared for larger
642 // numbers.
643 if (nFieldLen - nPrecision > nLen - nPrec)
644 nLen = nFieldLen - (nPrecision ? nPrecision+1 : 0) + 1 + nPrec;
645 // And override precision.
646 nPrecision = nPrec;
647 }
648 else
649 {
650 #if 1
651 // Adjust length to predefined precision.
652 nLen = nLen + ( nPrecision - nPrec );
653 #else
654 /* If the above override for (nPrecision < nPrec) was not in place then
655 * nPrecision could be 0 and this would be the code path to correctly
656 * calculate nLen. But as is, nPrecision is never 0 here, see CID#982304 */
657
658 // Adjust length to predefined precision.
659 if ( nPrecision )
660 nLen = nLen + ( nPrecision - nPrec );
661 else
662 nLen -= nPrec+1; // also remove the decimal separator
663 #endif
664 }
665 }
666 if (nFieldLen < nLen)
667 {
668 if (!bTypeDefined)
669 nFieldLen = nLen;
670 else
671 {
672 // Again a hairy case and conflict. Furthermore, the
673 // larger overall length may be a result of only a higher
674 // precision obtained from formats.
675 SAL_WARN( "sc", "lcl_GetColumnTypes: conflicting dBase field length for "
676 << aFieldName << " (" << nFieldLen << "<" << nLen << ")");
677 nFieldLen = nLen;
678 }
679 }
680 if ( !bPrecDefined )
681 nPrecision = nPrec;
682 if ( nFieldLen == 0 )
683 nFieldLen = 1;
684 else if ( nFieldLen > 19 )
685 nFieldLen = 19; // dBaseIII numeric field length limit: 19
686 if ( nPrecision && nFieldLen < nPrecision + 2 )
687 nFieldLen = nPrecision + 2; // 0. must fit into
688 // 538 MUST: Sdb internal representation adds 2 to the field length!
689 // To give the user what he wants we must subtract it here.
690 //! CAVEAT! There is no way to define a numeric field with a length
691 //! of 1 and no decimals!
692 nFieldLen = SvDbaseConverter::ConvertPrecisionToOdbc( nFieldLen, nPrecision );
693 }
694 if ( nFieldLen > 254 )
695 {
696 if ( nDbType == sdbc::DataType::VARCHAR )
697 { // Too long for a normal text field => memo field.
698 nDbType = sdbc::DataType::LONGVARCHAR;
699 nFieldLen = 10;
700 bHasMemo = true;
701 }
702 else
703 nFieldLen = 254; // bad luck...
704 }
705
706 pColNames[nField] = aFieldName;
707 pColTypes[nField] = nDbType;
708 pColLengths[nField] = nFieldLen;
709 pColScales[nField] = nPrecision;
710
711 ++nField;
712 }
713 }
714
lcl_getLongVarCharEditString(OUString & rString,const ScRefCellValue & rCell,ScFieldEditEngine & rEditEngine)715 void lcl_getLongVarCharEditString( OUString& rString,
716 const ScRefCellValue& rCell, ScFieldEditEngine& rEditEngine )
717 {
718 if (!rCell.getEditText())
719 return;
720
721 rEditEngine.SetTextCurrentDefaults(*rCell.getEditText());
722 rString = rEditEngine.GetText( LINEEND_CRLF );
723 }
724
lcl_getLongVarCharString(OUString & rString,ScDocument & rDoc,SCCOL nCol,SCROW nRow,SCTAB nTab,ScInterpreterContext & rContext)725 void lcl_getLongVarCharString(
726 OUString& rString, ScDocument& rDoc, SCCOL nCol, SCROW nRow, SCTAB nTab, ScInterpreterContext& rContext )
727 {
728 const Color* pColor;
729 ScAddress aPos(nCol, nRow, nTab);
730 sal_uInt32 nFormat = rDoc.GetNumberFormat(ScRange(aPos));
731 rString = ScCellFormat::GetString(rDoc, aPos, nFormat, &pColor, &rContext);
732 }
733
734 }
735
736 #endif // HAVE_FEATURE_DBCONNECTIVITY
737
DBaseExport(const OUString & rFullFileName,rtl_TextEncoding eCharSet,bool & bHasMemo)738 ErrCodeMsg ScDocShell::DBaseExport( const OUString& rFullFileName, rtl_TextEncoding eCharSet, bool& bHasMemo )
739 {
740 #if !HAVE_FEATURE_DBCONNECTIVITY
741 (void) rFullFileName;
742 (void) eCharSet;
743 (void) bHasMemo;
744
745 return ERRCODE_IO_GENERAL;
746 #else
747 // remove the file so the dBase driver doesn't find an invalid file
748 INetURLObject aDeleteObj( rFullFileName, INetProtocol::File );
749 KillFile( aDeleteObj );
750
751 ErrCodeMsg nErr = ERRCODE_NONE;
752
753 SCCOL nFirstCol, nLastCol;
754 SCROW nFirstRow, nLastRow;
755 SCTAB nTab = GetSaveTab();
756 m_pDocument->GetDataStart( nTab, nFirstCol, nFirstRow );
757 m_pDocument->GetCellArea( nTab, nLastCol, nLastRow );
758 if ( nFirstCol > nLastCol )
759 nFirstCol = nLastCol;
760 if ( nFirstRow > nLastRow )
761 nFirstRow = nLastRow;
762 ScProgress aProgress( this, ScResId( STR_SAVE_DOC ),
763 nLastRow - nFirstRow, true );
764 ScInterpreterContext& rContext = m_pDocument->GetNonThreadedContext();
765
766 bool bHasFieldNames = true;
767 for ( SCCOL nDocCol = nFirstCol; nDocCol <= nLastCol && bHasFieldNames; nDocCol++ )
768 { // only Strings in first row => are field names
769 if ( !m_pDocument->HasStringData( nDocCol, nFirstRow, nTab ) )
770 bHasFieldNames = false;
771 }
772
773 sal_Int32 nColCount = nLastCol - nFirstCol + 1;
774 uno::Sequence<OUString> aColNames( nColCount );
775 uno::Sequence<sal_Int32> aColTypes( nColCount );
776 uno::Sequence<sal_Int32> aColLengths( nColCount );
777 uno::Sequence<sal_Int32> aColScales( nColCount );
778
779 ScRange aDataRange( nFirstCol, nFirstRow, nTab, nLastCol, nLastRow, nTab );
780 lcl_GetColumnTypes( *this, aDataRange, bHasFieldNames,
781 aColNames.getArray(), aColTypes.getArray(),
782 aColLengths.getArray(), aColScales.getArray(),
783 bHasMemo, eCharSet );
784 // also needed for exception catch
785 SCROW nDocRow = 0;
786 ScFieldEditEngine aEditEngine(m_pDocument.get(), m_pDocument->GetEditEnginePool());
787 OUString aString;
788
789 try
790 {
791 uno::Reference<sdbc::XDriverManager2> xDrvMan;
792 uno::Reference<sdbc::XConnection> xConnection;
793 OUString aTabName;
794 ErrCode nRet = lcl_getDBaseConnection(xDrvMan,xConnection,aTabName,rFullFileName,eCharSet);
795 if ( !xConnection.is() || !xDrvMan.is() )
796 return nRet;
797 ::utl::DisposableComponent aConnectionHelper(xConnection);
798
799 // get dBase driver
800 uno::Reference< sdbcx::XDataDefinitionSupplier > xDDSup( xDrvMan->getDriverByURL( xConnection->getMetaData()->getURL() ), uno::UNO_QUERY );
801 if ( !xDDSup.is() )
802 return SCERR_EXPORT_CONNECT;
803
804 // create table
805 uno::Reference<sdbcx::XTablesSupplier> xTablesSupp =xDDSup->getDataDefinitionByConnection( xConnection );
806 OSL_ENSURE( xTablesSupp.is(), "can't get Data Definition" );
807 if (!xTablesSupp.is()) return SCERR_EXPORT_CONNECT;
808
809 uno::Reference<container::XNameAccess> xTables = xTablesSupp->getTables();
810 OSL_ENSURE( xTables.is(), "can't get Tables" );
811 if (!xTables.is()) return SCERR_EXPORT_CONNECT;
812
813 uno::Reference<sdbcx::XDataDescriptorFactory> xTablesFact( xTables, uno::UNO_QUERY );
814 OSL_ENSURE( xTablesFact.is(), "can't get tables factory" );
815 if (!xTablesFact.is()) return SCERR_EXPORT_CONNECT;
816
817 uno::Reference<sdbcx::XAppend> xTablesAppend( xTables, uno::UNO_QUERY );
818 OSL_ENSURE( xTablesAppend.is(), "can't get tables XAppend" );
819 if (!xTablesAppend.is()) return SCERR_EXPORT_CONNECT;
820
821 uno::Reference<beans::XPropertySet> xTableDesc = xTablesFact->createDataDescriptor();
822 OSL_ENSURE( xTableDesc.is(), "can't get table descriptor" );
823 if (!xTableDesc.is()) return SCERR_EXPORT_CONNECT;
824
825 xTableDesc->setPropertyValue( SC_DBPROP_NAME, uno::Any(aTabName) );
826
827 // create columns
828
829 uno::Reference<sdbcx::XColumnsSupplier> xColumnsSupp( xTableDesc, uno::UNO_QUERY );
830 OSL_ENSURE( xColumnsSupp.is(), "can't get columns supplier" );
831 if (!xColumnsSupp.is()) return SCERR_EXPORT_CONNECT;
832
833 uno::Reference<container::XNameAccess> xColumns = xColumnsSupp->getColumns();
834 OSL_ENSURE( xColumns.is(), "can't get columns" );
835 if (!xColumns.is()) return SCERR_EXPORT_CONNECT;
836
837 uno::Reference<sdbcx::XDataDescriptorFactory> xColumnsFact( xColumns, uno::UNO_QUERY );
838 OSL_ENSURE( xColumnsFact.is(), "can't get columns factory" );
839 if (!xColumnsFact.is()) return SCERR_EXPORT_CONNECT;
840
841 uno::Reference<sdbcx::XAppend> xColumnsAppend( xColumns, uno::UNO_QUERY );
842 OSL_ENSURE( xColumnsAppend.is(), "can't get columns XAppend" );
843 if (!xColumnsAppend.is()) return SCERR_EXPORT_CONNECT;
844
845 const OUString* pColNames = aColNames.getConstArray();
846 const sal_Int32* pColTypes = aColTypes.getConstArray();
847 const sal_Int32* pColLengths = aColLengths.getConstArray();
848 const sal_Int32* pColScales = aColScales.getConstArray();
849 sal_Int32 nCol;
850
851 for (nCol=0; nCol<nColCount; nCol++)
852 {
853 uno::Reference<beans::XPropertySet> xColumnDesc = xColumnsFact->createDataDescriptor();
854 OSL_ENSURE( xColumnDesc.is(), "can't get column descriptor" );
855 if (!xColumnDesc.is()) return SCERR_EXPORT_CONNECT;
856
857 xColumnDesc->setPropertyValue( SC_DBPROP_NAME, uno::Any(pColNames[nCol]) );
858
859 xColumnDesc->setPropertyValue( SC_DBPROP_TYPE, uno::Any(pColTypes[nCol]) );
860
861 xColumnDesc->setPropertyValue( SC_DBPROP_PRECISION, uno::Any(pColLengths[nCol]) );
862
863 xColumnDesc->setPropertyValue( SC_DBPROP_SCALE, uno::Any(pColScales[nCol]) );
864
865 xColumnsAppend->appendByDescriptor( xColumnDesc );
866 }
867
868 xTablesAppend->appendByDescriptor( xTableDesc );
869
870 // get row set for writing
871 uno::Reference<lang::XMultiServiceFactory> xFactory = comphelper::getProcessServiceFactory();
872 uno::Reference<sdbc::XRowSet> xRowSet( xFactory->createInstance(SC_SERVICE_ROWSET),
873 uno::UNO_QUERY);
874 ::utl::DisposableComponent aRowSetHelper(xRowSet);
875 uno::Reference<beans::XPropertySet> xRowProp( xRowSet, uno::UNO_QUERY );
876 OSL_ENSURE( xRowProp.is(), "can't get RowSet" );
877 if (!xRowProp.is()) return SCERR_EXPORT_CONNECT;
878
879 xRowProp->setPropertyValue( SC_DBPROP_ACTIVECONNECTION, uno::Any(xConnection) );
880
881 xRowProp->setPropertyValue( SC_DBPROP_COMMANDTYPE, uno::Any(sal_Int32(sdb::CommandType::TABLE)) );
882
883 xRowProp->setPropertyValue( SC_DBPROP_COMMAND, uno::Any(aTabName) );
884
885 xRowSet->execute();
886
887 // write data rows
888
889 uno::Reference<sdbc::XResultSetUpdate> xResultUpdate( xRowSet, uno::UNO_QUERY );
890 OSL_ENSURE( xResultUpdate.is(), "can't get XResultSetUpdate" );
891 if (!xResultUpdate.is()) return SCERR_EXPORT_CONNECT;
892
893 uno::Reference<sdbc::XRowUpdate> xRowUpdate( xRowSet, uno::UNO_QUERY );
894 OSL_ENSURE( xRowUpdate.is(), "can't get XRowUpdate" );
895 if (!xRowUpdate.is()) return SCERR_EXPORT_CONNECT;
896
897 SCROW nFirstDataRow = ( bHasFieldNames ? nFirstRow + 1 : nFirstRow );
898 double fVal;
899
900 for ( nDocRow = nFirstDataRow; nDocRow <= nLastRow; nDocRow++ )
901 {
902 xResultUpdate->moveToInsertRow();
903
904 for (nCol=0; nCol<nColCount; nCol++)
905 {
906 SCCOL nDocCol = sal::static_int_cast<SCCOL>( nFirstCol + nCol );
907
908 switch (pColTypes[nCol])
909 {
910 case sdbc::DataType::LONGVARCHAR:
911 {
912 ScRefCellValue aCell(*m_pDocument, ScAddress(nDocCol, nDocRow, nTab));
913 if (!aCell.isEmpty())
914 {
915 if (aCell.getType() == CELLTYPE_EDIT)
916 { // preserve paragraphs
917 lcl_getLongVarCharEditString(aString, aCell, aEditEngine);
918 }
919 else
920 {
921 lcl_getLongVarCharString(
922 aString, *m_pDocument, nDocCol, nDocRow, nTab, rContext);
923 }
924 xRowUpdate->updateString( nCol+1, aString );
925 }
926 else
927 xRowUpdate->updateNull( nCol+1 );
928 }
929 break;
930
931 case sdbc::DataType::VARCHAR:
932 aString = m_pDocument->GetString(nDocCol, nDocRow, nTab);
933 xRowUpdate->updateString( nCol+1, aString );
934 if ( nErr == ERRCODE_NONE && pColLengths[nCol] < aString.getLength() )
935 nErr = SCWARN_EXPORT_DATALOST;
936 break;
937
938 case sdbc::DataType::DATE:
939 {
940 fVal = m_pDocument->GetValue( nDocCol, nDocRow, nTab );
941 // differentiate between 0 with value and 0 no-value
942 bool bIsNull = (fVal == 0.0);
943 if ( bIsNull )
944 bIsNull = !m_pDocument->HasValueData( nDocCol, nDocRow, nTab );
945 if ( bIsNull )
946 {
947 xRowUpdate->updateNull( nCol+1 );
948 if ( nErr == ERRCODE_NONE &&
949 m_pDocument->HasStringData( nDocCol, nDocRow, nTab ) )
950 nErr = SCWARN_EXPORT_DATALOST;
951 }
952 else
953 {
954 Date aDate = rContext.NFGetNullDate(); // tools date
955 aDate.AddDays(fVal); //! approxfloor?
956 xRowUpdate->updateDate( nCol+1, aDate.GetUNODate() );
957 }
958 }
959 break;
960
961 case sdbc::DataType::DECIMAL:
962 case sdbc::DataType::BIT:
963 fVal = m_pDocument->GetValue( nDocCol, nDocRow, nTab );
964 if ( fVal == 0.0 && nErr == ERRCODE_NONE &&
965 m_pDocument->HasStringData( nDocCol, nDocRow, nTab ) )
966 nErr = SCWARN_EXPORT_DATALOST;
967 if ( pColTypes[nCol] == sdbc::DataType::BIT )
968 xRowUpdate->updateBoolean( nCol+1, ( fVal != 0.0 ) );
969 else
970 xRowUpdate->updateDouble( nCol+1, fVal );
971 break;
972
973 default:
974 OSL_FAIL( "ScDocShell::DBaseExport: unknown FieldType" );
975 if ( nErr == ERRCODE_NONE )
976 nErr = SCWARN_EXPORT_DATALOST;
977 fVal = m_pDocument->GetValue( nDocCol, nDocRow, nTab );
978 xRowUpdate->updateDouble( nCol+1, fVal );
979 }
980 }
981
982 xResultUpdate->insertRow();
983
984 //! error handling and recovery of old
985 //! ScDocShell::SbaSdbExport is still missing!
986
987 aProgress.SetStateOnPercent( nDocRow - nFirstRow );
988 }
989
990 comphelper::disposeComponent( xRowSet );
991 comphelper::disposeComponent( xConnection );
992 }
993 catch ( const sdbc::SQLException& aException )
994 {
995 sal_Int32 nError = aException.ErrorCode;
996 TOOLS_WARN_EXCEPTION("sc", "ScDocShell::DBaseExport");
997
998 if (nError == 22018 || nError == 22001)
999 {
1000 // SQL error 22018: Character not in target encoding.
1001 // SQL error 22001: String length exceeds field width (after encoding).
1002 bool bEncErr = (nError == 22018);
1003 bool bIsOctetTextEncoding = rtl_isOctetTextEncoding( eCharSet);
1004 OSL_ENSURE( !bEncErr || bIsOctetTextEncoding, "ScDocShell::DBaseExport: encoding error and not an octet textencoding");
1005 SCCOL nDocCol = nFirstCol;
1006 const sal_Int32* pColTypes = aColTypes.getConstArray();
1007 const sal_Int32* pColLengths = aColLengths.getConstArray();
1008 ScHorizontalCellIterator aIter( *m_pDocument, nTab, nFirstCol,
1009 nDocRow, nLastCol, nDocRow);
1010 bool bTest = true;
1011 while (bTest)
1012 {
1013 ScRefCellValue* pCell = aIter.GetNext( nDocCol, nDocRow);
1014 if (!pCell)
1015 break;
1016 SCCOL nCol = nDocCol - nFirstCol;
1017 switch (pColTypes[nCol])
1018 {
1019 case sdbc::DataType::LONGVARCHAR:
1020 {
1021 if (pCell->getType() == CELLTYPE_EDIT)
1022 lcl_getLongVarCharEditString(aString, *pCell, aEditEngine);
1023 else
1024 lcl_getLongVarCharString(
1025 aString, *m_pDocument, nDocCol, nDocRow, nTab, rContext);
1026 }
1027 break;
1028
1029 case sdbc::DataType::VARCHAR:
1030 aString = m_pDocument->GetString(nDocCol, nDocRow, nTab);
1031 break;
1032
1033 // NOTE: length of DECIMAL fields doesn't need to be
1034 // checked here, the database driver adjusts the field
1035 // width accordingly.
1036
1037 default:
1038 bTest = false;
1039 }
1040 if (bTest)
1041 {
1042 sal_Int32 nLen;
1043 if (bIsOctetTextEncoding)
1044 {
1045 OString aOString;
1046 if (!aString.convertToString( &aOString, eCharSet,
1047 RTL_UNICODETOTEXT_FLAGS_UNDEFINED_ERROR |
1048 RTL_UNICODETOTEXT_FLAGS_INVALID_ERROR))
1049 {
1050 bTest = false;
1051 bEncErr = true;
1052 }
1053 nLen = aOString.getLength();
1054 if (!bTest)
1055 SAL_WARN("sc", "ScDocShell::DBaseExport encoding error, string with default replacements: ``" << aString << "''");
1056 }
1057 else
1058 nLen = aString.getLength() * sizeof(sal_Unicode);
1059 if (!bEncErr &&
1060 pColTypes[nCol] != sdbc::DataType::LONGVARCHAR &&
1061 pColLengths[nCol] < nLen)
1062 {
1063 bTest = false;
1064 SAL_INFO("sc", "ScDocShell::DBaseExport: field width: " << pColLengths[nCol] << ", encoded length: " << nLen);
1065 }
1066 }
1067 else
1068 bTest = true;
1069 }
1070 OUString sPosition(ScAddress(nDocCol, nDocRow, nTab).GetColRowString());
1071 OUString sEncoding(SvxTextEncodingTable::GetTextString(eCharSet));
1072 nErr = ErrCodeMsg( (bEncErr ? SCERR_EXPORT_ENCODING :
1073 SCERR_EXPORT_FIELDWIDTH), sPosition, sEncoding,
1074 DialogMask::ButtonsOk | DialogMask::MessageError);
1075 }
1076 else if ( !aException.Message.isEmpty() )
1077 nErr = ErrCodeMsg( SCERR_EXPORT_SQLEXCEPTION, aException.Message, DialogMask::ButtonsOk | DialogMask::MessageError);
1078 else
1079 nErr = SCERR_EXPORT_DATA;
1080 }
1081 catch ( uno::Exception& )
1082 {
1083 TOOLS_WARN_EXCEPTION( "sc", "Unexpected exception in database");
1084 nErr = ERRCODE_IO_GENERAL;
1085 }
1086
1087 return nErr;
1088 #endif // HAVE_FEATURE_DBCONNECTIVITY
1089 }
1090
1091 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1092