1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
20 #include <calc/CTable.hxx>
21 #include <com/sun/star/sdbc/ColumnValue.hpp>
22 #include <com/sun/star/sdbc/DataType.hpp>
23 #include <com/sun/star/sdbc/SQLException.hpp>
24 #include <com/sun/star/sheet/XSpreadsheetDocument.hpp>
25 #include <com/sun/star/sheet/XSpreadsheet.hpp>
26 #include <com/sun/star/sheet/XCellRangeAddressable.hpp>
27 #include <com/sun/star/sheet/XCellRangesQuery.hpp>
28 #include <com/sun/star/sheet/XDatabaseRanges.hpp>
29 #include <com/sun/star/sheet/XDatabaseRange.hpp>
30 #include <com/sun/star/sheet/XCellRangeReferrer.hpp>
31 #include <com/sun/star/sheet/XUsedAreaCursor.hpp>
32 #include <com/sun/star/sheet/CellFlags.hpp>
33 #include <com/sun/star/sheet/FormulaResult.hpp>
34 #include <com/sun/star/util/NumberFormat.hpp>
35 #include <com/sun/star/util/XNumberFormatsSupplier.hpp>
36 #include <com/sun/star/text/XText.hpp>
37 #include <calc/CConnection.hxx>
38 #include <connectivity/sdbcx/VColumn.hxx>
39 #include <rtl/ustrbuf.hxx>
40 #include <sal/log.hxx>
41 #include <rtl/math.hxx>
42 #include <tools/time.hxx>
43 #include <comphelper/servicehelper.hxx>
44 #include <cppuhelper/typeprovider.hxx>
46 using namespace connectivity
;
47 using namespace connectivity::calc
;
48 using namespace connectivity::file
;
49 using namespace ::cppu
;
50 using namespace ::dbtools
;
51 using namespace ::com::sun::star::uno
;
52 using namespace ::com::sun::star::beans
;
53 using namespace ::com::sun::star::sdbcx
;
54 using namespace ::com::sun::star::sdbc
;
55 using namespace ::com::sun::star::container
;
56 using namespace ::com::sun::star::lang
;
57 using namespace ::com::sun::star::sheet
;
58 using namespace ::com::sun::star::table
;
59 using namespace ::com::sun::star::text
;
60 using namespace ::com::sun::star::util
;
63 static void lcl_UpdateArea( const Reference
<XCellRange
>& xUsedRange
, sal_Int32
& rEndCol
, sal_Int32
& rEndRow
)
65 // update rEndCol, rEndRow if any non-empty cell in xUsedRange is right/below
67 const Reference
<XCellRangesQuery
> xUsedQuery( xUsedRange
, UNO_QUERY
);
68 if ( !xUsedQuery
.is() )
71 const sal_Int16 nContentFlags
=
72 CellFlags::STRING
| CellFlags::VALUE
| CellFlags::DATETIME
| CellFlags::FORMULA
| CellFlags::ANNOTATION
;
74 const Reference
<XSheetCellRanges
> xUsedRanges
= xUsedQuery
->queryContentCells( nContentFlags
);
75 const Sequence
<CellRangeAddress
> aAddresses
= xUsedRanges
->getRangeAddresses();
77 const sal_Int32 nCount
= aAddresses
.getLength();
78 const CellRangeAddress
* pData
= aAddresses
.getConstArray();
79 for ( sal_Int32 i
=0; i
<nCount
; i
++ )
81 rEndCol
= std::max(pData
[i
].EndColumn
, rEndCol
);
82 rEndRow
= std::max(pData
[i
].EndRow
, rEndRow
);
86 static void lcl_GetDataArea( const Reference
<XSpreadsheet
>& xSheet
, sal_Int32
& rColumnCount
, sal_Int32
& rRowCount
)
88 Reference
<XSheetCellCursor
> xCursor
= xSheet
->createCursor();
89 Reference
<XCellRangeAddressable
> xRange( xCursor
, UNO_QUERY
);
92 rColumnCount
= rRowCount
= 0;
96 // first find the contiguous cell area starting at A1
98 xCursor
->collapseToSize( 1, 1 ); // single (first) cell
99 xCursor
->collapseToCurrentRegion(); // contiguous data area
101 CellRangeAddress aRegionAddr
= xRange
->getRangeAddress();
102 sal_Int32 nEndCol
= aRegionAddr
.EndColumn
;
103 sal_Int32 nEndRow
= aRegionAddr
.EndRow
;
105 Reference
<XUsedAreaCursor
> xUsed( xCursor
, UNO_QUERY
);
108 // The used area from XUsedAreaCursor includes visible attributes.
109 // If the used area is larger than the contiguous cell area, find non-empty
110 // cells in that area.
112 xUsed
->gotoEndOfUsedArea( false );
113 CellRangeAddress aUsedAddr
= xRange
->getRangeAddress();
115 if ( aUsedAddr
.EndColumn
> aRegionAddr
.EndColumn
)
117 Reference
<XCellRange
> xUsedRange
= xSheet
->getCellRangeByPosition(
118 aRegionAddr
.EndColumn
+ 1, 0, aUsedAddr
.EndColumn
, aUsedAddr
.EndRow
);
119 lcl_UpdateArea( xUsedRange
, nEndCol
, nEndRow
);
122 if ( aUsedAddr
.EndRow
> aRegionAddr
.EndRow
)
124 // only up to the last column of aRegionAddr, the other columns are handled above
125 Reference
<XCellRange
> xUsedRange
= xSheet
->getCellRangeByPosition(
126 0, aRegionAddr
.EndRow
+ 1, aRegionAddr
.EndColumn
, aUsedAddr
.EndRow
);
127 lcl_UpdateArea( xUsedRange
, nEndCol
, nEndRow
);
131 rColumnCount
= nEndCol
+ 1; // number of columns
132 rRowCount
= nEndRow
; // first row (headers) is not counted
135 static CellContentType
lcl_GetContentOrResultType( const Reference
<XCell
>& xCell
)
137 CellContentType eCellType
= xCell
->getType();
138 if ( eCellType
== CellContentType_FORMULA
)
140 Reference
<XPropertySet
> xProp( xCell
, UNO_QUERY
);
143 xProp
->getPropertyValue( "CellContentType" ) >>= eCellType
; // type of cell content
145 catch (UnknownPropertyException
&)
147 eCellType
= CellContentType_VALUE
; // if CellContentType property not available
153 static Reference
<XCell
> lcl_GetUsedCell( const Reference
<XSpreadsheet
>& xSheet
, sal_Int32 nDocColumn
, sal_Int32 nDocRow
)
155 Reference
<XCell
> xCell
= xSheet
->getCellByPosition( nDocColumn
, nDocRow
);
156 if ( xCell
.is() && xCell
->getType() == CellContentType_EMPTY
)
158 // get first non-empty cell
160 Reference
<XCellRangeAddressable
> xAddr( xSheet
, UNO_QUERY
);
163 CellRangeAddress aTotalRange
= xAddr
->getRangeAddress();
164 sal_Int32 nLastRow
= aTotalRange
.EndRow
;
165 Reference
<XCellRangesQuery
> xQuery( xSheet
->getCellRangeByPosition( nDocColumn
, nDocRow
, nDocColumn
, nLastRow
), UNO_QUERY
);
168 // queryIntersection to get a ranges object
169 Reference
<XSheetCellRanges
> xRanges
= xQuery
->queryIntersection( aTotalRange
);
172 Reference
<XEnumerationAccess
> xCells
= xRanges
->getCells();
175 Reference
<XEnumeration
> xEnum
= xCells
->createEnumeration();
176 if ( xEnum
.is() && xEnum
->hasMoreElements() )
178 // get first non-empty cell from enumeration
179 xCell
.set(xEnum
->nextElement(),UNO_QUERY
);
181 // otherwise, keep empty cell
190 static bool lcl_HasTextInColumn( const Reference
<XSpreadsheet
>& xSheet
, sal_Int32 nDocColumn
, sal_Int32 nDocRow
)
192 // look for any text cell or text result in the column
194 Reference
<XCellRangeAddressable
> xAddr( xSheet
, UNO_QUERY
);
197 CellRangeAddress aTotalRange
= xAddr
->getRangeAddress();
198 sal_Int32 nLastRow
= aTotalRange
.EndRow
;
199 Reference
<XCellRangesQuery
> xQuery( xSheet
->getCellRangeByPosition( nDocColumn
, nDocRow
, nDocColumn
, nLastRow
), UNO_QUERY
);
202 // are there text cells in the column?
203 Reference
<XSheetCellRanges
> xTextContent
= xQuery
->queryContentCells( CellFlags::STRING
);
204 if ( xTextContent
.is() && xTextContent
->hasElements() )
207 // are there formulas with text results in the column?
208 Reference
<XSheetCellRanges
> xTextFormula
= xQuery
->queryFormulaCells( FormulaResult::STRING
);
209 if ( xTextFormula
.is() && xTextFormula
->hasElements() )
217 static void lcl_GetColumnInfo( const Reference
<XSpreadsheet
>& xSheet
, const Reference
<XNumberFormats
>& xFormats
,
218 sal_Int32 nDocColumn
, sal_Int32 nStartRow
, bool bHasHeaders
,
219 OUString
& rName
, sal_Int32
& rDataType
, bool& rCurrency
)
221 //! avoid duplicate field names
223 // get column name from first row, if range contains headers
227 Reference
<XText
> xHeaderText( xSheet
->getCellByPosition( nDocColumn
, nStartRow
), UNO_QUERY
);
228 if ( xHeaderText
.is() )
229 rName
= xHeaderText
->getString();
232 // get column type from first data row
234 sal_Int32 nDataRow
= nStartRow
;
237 Reference
<XCell
> xDataCell
= lcl_GetUsedCell( xSheet
, nDocColumn
, nDataRow
);
239 Reference
<XPropertySet
> xProp( xDataCell
, UNO_QUERY
);
243 rCurrency
= false; // set to true for currency below
245 const CellContentType eCellType
= lcl_GetContentOrResultType( xDataCell
);
246 // #i35178# use "text" type if there is any text cell in the column
247 if ( eCellType
== CellContentType_TEXT
|| lcl_HasTextInColumn( xSheet
, nDocColumn
, nDataRow
) )
248 rDataType
= DataType::VARCHAR
;
249 else if ( eCellType
== CellContentType_VALUE
)
251 // get number format to distinguish between different types
253 sal_Int16 nNumType
= NumberFormat::NUMBER
;
258 if ( xProp
->getPropertyValue( "NumberFormat" ) >>= nKey
)
260 const Reference
<XPropertySet
> xFormat
= xFormats
->getByKey( nKey
);
263 xFormat
->getPropertyValue( OMetaConnection::getPropMap().getNameByIndex(PROPERTY_ID_TYPE
) ) >>= nNumType
;
271 if ( nNumType
& NumberFormat::TEXT
)
272 rDataType
= DataType::VARCHAR
;
273 else if ( nNumType
& NumberFormat::NUMBER
)
274 rDataType
= DataType::DECIMAL
;
275 else if ( nNumType
& NumberFormat::CURRENCY
)
278 rDataType
= DataType::DECIMAL
;
280 else if ( ( nNumType
& NumberFormat::DATETIME
) == NumberFormat::DATETIME
)
282 // NumberFormat::DATETIME is DATE | TIME
283 rDataType
= DataType::TIMESTAMP
;
285 else if ( nNumType
& NumberFormat::DATE
)
286 rDataType
= DataType::DATE
;
287 else if ( nNumType
& NumberFormat::TIME
)
288 rDataType
= DataType::TIME
;
289 else if ( nNumType
& NumberFormat::LOGICAL
)
290 rDataType
= DataType::BIT
;
292 rDataType
= DataType::DECIMAL
;
296 // whole column empty
297 rDataType
= DataType::VARCHAR
;
302 static void lcl_SetValue( ORowSetValue
& rValue
, const Reference
<XSpreadsheet
>& xSheet
,
303 sal_Int32 nStartCol
, sal_Int32 nStartRow
, bool bHasHeaders
,
304 const ::Date
& rNullDate
,
305 sal_Int32 nDBRow
, sal_Int32 nDBColumn
, sal_Int32 nType
)
307 sal_Int32 nDocColumn
= nStartCol
+ nDBColumn
- 1; // database counts from 1
308 sal_Int32 nDocRow
= nStartRow
+ nDBRow
- 1;
312 const Reference
<XCell
> xCell
= xSheet
->getCellByPosition( nDocColumn
, nDocRow
);
316 CellContentType eCellType
= lcl_GetContentOrResultType( xCell
);
319 case DataType::VARCHAR
:
320 if ( eCellType
== CellContentType_EMPTY
)
324 // #i25840# still let Calc convert numbers to text
325 const Reference
<XText
> xText( xCell
, UNO_QUERY
);
327 rValue
= xText
->getString();
330 case DataType::DECIMAL
:
331 if ( eCellType
== CellContentType_VALUE
)
332 rValue
= xCell
->getValue(); // double
337 if ( eCellType
== CellContentType_VALUE
)
338 rValue
= xCell
->getValue() != 0.0;
343 if ( eCellType
== CellContentType_VALUE
)
345 ::Date
aDate( rNullDate
);
346 aDate
.AddDays(::rtl::math::approxFloor( xCell
->getValue() ));
347 rValue
= aDate
.GetUNODate();
353 if ( eCellType
== CellContentType_VALUE
)
355 double fCellVal
= xCell
->getValue();
356 double fTime
= fCellVal
- rtl::math::approxFloor( fCellVal
);
357 sal_Int64 nIntTime
= static_cast<sal_Int64
>(rtl::math::round( fTime
* static_cast<double>(::tools::Time::nanoSecPerDay
) ));
358 if ( nIntTime
== ::tools::Time::nanoSecPerDay
)
359 nIntTime
= 0; // 23:59:59.9999999995 and above is 00:00:00.00
360 css::util::Time aTime
;
361 aTime
.NanoSeconds
= static_cast<sal_uInt32
>( nIntTime
% ::tools::Time::nanoSecPerSec
);
362 nIntTime
/= ::tools::Time::nanoSecPerSec
;
363 aTime
.Seconds
= static_cast<sal_uInt16
>( nIntTime
% 60 );
365 aTime
.Minutes
= static_cast<sal_uInt16
>( nIntTime
% 60 );
367 OSL_ENSURE( nIntTime
< 24, "error in time calculation" );
368 aTime
.Hours
= static_cast<sal_uInt16
>(nIntTime
);
374 case DataType::TIMESTAMP
:
375 if ( eCellType
== CellContentType_VALUE
)
377 double fCellVal
= xCell
->getValue();
378 double fDays
= ::rtl::math::approxFloor( fCellVal
);
379 double fTime
= fCellVal
- fDays
;
380 tools::Long nIntDays
= static_cast<tools::Long
>(fDays
);
381 sal_Int64 nIntTime
= ::rtl::math::round( fTime
* static_cast<double>(::tools::Time::nanoSecPerDay
) );
382 if ( nIntTime
== ::tools::Time::nanoSecPerDay
)
384 nIntTime
= 0; // 23:59:59.9999999995 and above is 00:00:00.00
385 ++nIntDays
; // (next day)
388 css::util::DateTime aDateTime
;
390 aDateTime
.NanoSeconds
= static_cast<sal_uInt16
>( nIntTime
% ::tools::Time::nanoSecPerSec
);
391 nIntTime
/= ::tools::Time::nanoSecPerSec
;
392 aDateTime
.Seconds
= static_cast<sal_uInt16
>( nIntTime
% 60 );
394 aDateTime
.Minutes
= static_cast<sal_uInt16
>( nIntTime
% 60 );
396 OSL_ENSURE( nIntTime
< 24, "error in time calculation" );
397 aDateTime
.Hours
= static_cast<sal_uInt16
>(nIntTime
);
399 ::Date
aDate( rNullDate
);
400 aDate
.AddDays( nIntDays
);
401 aDateTime
.Day
= aDate
.GetDay();
402 aDateTime
.Month
= aDate
.GetMonth();
403 aDateTime
.Year
= aDate
.GetYear();
412 // rValue.setTypeKind(nType);
416 static OUString
lcl_GetColumnStr( sal_Int32 nColumn
)
419 return OUString( static_cast<sal_Unicode
>( 'A' + nColumn
) );
422 OUStringBuffer
aBuffer(2);
423 aBuffer
.setLength( 2 );
424 aBuffer
[0] = static_cast<sal_Unicode
>( 'A' + ( nColumn
/ 26 ) - 1 );
425 aBuffer
[1] = static_cast<sal_Unicode
>( 'A' + ( nColumn
% 26 ) );
426 return aBuffer
.makeStringAndClear();
430 void OCalcTable::fillColumns()
432 if ( !m_xSheet
.is() )
433 throw SQLException();
436 ::comphelper::UStringMixEqual
aCase(m_pConnection
->getMetaData()->supportsMixedCaseQuotedIdentifiers());
437 const bool bStoresMixedCaseQuotedIdentifiers
= getConnection()->getMetaData()->supportsMixedCaseQuotedIdentifiers();
439 for (sal_Int32 i
= 0; i
< m_nDataCols
; i
++)
441 OUString aColumnName
;
442 sal_Int32 eType
= DataType::OTHER
;
443 bool bCurrency
= false;
445 lcl_GetColumnInfo( m_xSheet
, m_xFormats
, m_nStartCol
+ i
, m_nStartRow
, m_bHasHeaders
,
446 aColumnName
, eType
, bCurrency
);
448 if ( aColumnName
.isEmpty() )
449 aColumnName
= lcl_GetColumnStr( i
);
451 sal_Int32 nPrecision
= 0; //! ...
452 sal_Int32 nDecimals
= 0; //! ...
456 case DataType::VARCHAR
:
457 aTypeName
= "VARCHAR";
459 case DataType::DECIMAL
:
460 aTypeName
= "DECIMAL";
471 case DataType::TIMESTAMP
:
472 aTypeName
= "TIMESTAMP";
475 SAL_WARN( "connectivity.drivers","missing type name");
479 // check if the column name already exists
480 OUString aAlias
= aColumnName
;
481 OSQLColumns::const_iterator aFind
= connectivity::find(m_aColumns
->begin(),m_aColumns
->end(),aAlias
,aCase
);
482 sal_Int32 nExprCnt
= 0;
483 while(aFind
!= m_aColumns
->end())
485 aAlias
= aColumnName
+ OUString::number(++nExprCnt
);
486 aFind
= connectivity::find(m_aColumns
->begin(),m_aColumns
->end(),aAlias
,aCase
);
489 rtl::Reference
<sdbcx::OColumn
> pColumn
= new sdbcx::OColumn( aAlias
, aTypeName
, OUString(),OUString(),
490 ColumnValue::NULLABLE
, nPrecision
, nDecimals
,
491 eType
, false, false, bCurrency
,
492 bStoresMixedCaseQuotedIdentifiers
,
493 m_CatalogName
, getSchema(), getName());
494 m_aColumns
->push_back(pColumn
);
495 m_aTypes
.push_back(eType
);
500 OCalcTable::OCalcTable(sdbcx::OCollection
* _pTables
,OCalcConnection
* _pConnection
,
501 const OUString
& Name
,
502 const OUString
& Type
,
503 const OUString
& Description
,
504 const OUString
& SchemaName
,
505 const OUString
& CatalogName
506 ) : OCalcTable_BASE(_pTables
,_pConnection
,Name
,
511 ,m_pCalcConnection(_pConnection
)
515 ,m_bHasHeaders(false)
516 ,m_aNullDate(::Date::EMPTY
)
520 void OCalcTable::construct()
523 Reference
< XSpreadsheetDocument
> xDoc
= m_pCalcConnection
->acquireDoc();
526 Reference
<XSpreadsheets
> xSheets
= xDoc
->getSheets();
527 if ( xSheets
.is() && xSheets
->hasByName( m_Name
) )
529 m_xSheet
.set(xSheets
->getByName( m_Name
),UNO_QUERY
);
532 lcl_GetDataArea( m_xSheet
, m_nDataCols
, m_nDataRows
);
533 m_bHasHeaders
= true;
534 // whole sheet is always assumed to include a header row
537 else // no sheet -> try database range
539 Reference
<XPropertySet
> xDocProp( xDoc
, UNO_QUERY
);
542 Reference
<XDatabaseRanges
> xRanges(xDocProp
->getPropertyValue("DatabaseRanges"),UNO_QUERY
);
544 if ( xRanges
.is() && xRanges
->hasByName( m_Name
) )
546 Reference
<XDatabaseRange
> xDBRange(xRanges
->getByName( m_Name
),UNO_QUERY
);
547 Reference
<XCellRangeReferrer
> xRefer( xDBRange
, UNO_QUERY
);
550 // Header flag is always stored with database range
551 // Get flag from FilterDescriptor
553 bool bRangeHeader
= true;
554 Reference
<XPropertySet
> xFiltProp( xDBRange
->getFilterDescriptor(), UNO_QUERY
);
555 if ( xFiltProp
.is() )
556 xFiltProp
->getPropertyValue("ContainsHeader") >>= bRangeHeader
;
558 Reference
<XSheetCellRange
> xSheetRange( xRefer
->getReferredCells(), UNO_QUERY
);
559 Reference
<XCellRangeAddressable
> xAddr( xSheetRange
, UNO_QUERY
);
560 if ( xSheetRange
.is() && xAddr
.is() )
562 m_xSheet
= xSheetRange
->getSpreadsheet();
563 CellRangeAddress aRangeAddr
= xAddr
->getRangeAddress();
564 m_nStartCol
= aRangeAddr
.StartColumn
;
565 m_nStartRow
= aRangeAddr
.StartRow
;
566 m_nDataCols
= aRangeAddr
.EndColumn
- m_nStartCol
+ 1;
567 // m_nDataRows is excluding header row
568 m_nDataRows
= aRangeAddr
.EndRow
- m_nStartRow
;
571 // m_nDataRows counts the whole range
575 m_bHasHeaders
= bRangeHeader
;
582 Reference
<XNumberFormatsSupplier
> xSupp( xDoc
, UNO_QUERY
);
584 m_xFormats
= xSupp
->getNumberFormats();
586 Reference
<XPropertySet
> xProp( xDoc
, UNO_QUERY
);
589 css::util::Date aDateStruct
;
590 if ( xProp
->getPropertyValue("NullDate") >>= aDateStruct
)
591 m_aNullDate
= ::Date( aDateStruct
.Day
, aDateStruct
.Month
, aDateStruct
.Year
);
595 //! default if no null date available?
602 void SAL_CALL
OCalcTable::disposing()
604 OFileTable::disposing();
605 ::osl::MutexGuard
aGuard(m_aMutex
);
606 m_aColumns
= nullptr;
607 if ( m_pCalcConnection
)
608 m_pCalcConnection
->releaseDoc();
609 m_pCalcConnection
= nullptr;
613 Sequence
< sal_Int8
> OCalcTable::getUnoTunnelId()
615 static ::cppu::OImplementationId implId
;
617 return implId
.getImplementationId();
620 // css::lang::XUnoTunnel
622 sal_Int64
OCalcTable::getSomething( const Sequence
< sal_Int8
> & rId
)
624 return (isUnoTunnelId
<OCalcTable
>(rId
))
625 ? reinterpret_cast< sal_Int64
>( this )
626 : OCalcTable_BASE::getSomething(rId
);
629 bool OCalcTable::fetchRow( OValueRefRow
& _rRow
, const OSQLColumns
& _rCols
,
634 _rRow
->setDeleted(false);
635 *(*_rRow
)[0] = m_nFilePos
;
642 const OValueRefVector::size_type nCount
= std::min(_rRow
->size(), _rCols
.size() + 1);
643 for (OValueRefVector::size_type i
= 1; i
< nCount
; i
++)
645 if ( (*_rRow
)[i
]->isBound() )
647 sal_Int32 nType
= m_aTypes
[i
-1];
649 lcl_SetValue( (*_rRow
)[i
]->get(), m_xSheet
, m_nStartCol
, m_nStartRow
, m_bHasHeaders
,
650 m_aNullDate
, m_nFilePos
, i
, nType
);
656 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */