summaryrefslogtreecommitdiffstats
path: root/connectivity/source/commontools/TTableHelper.cxx
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--connectivity/source/commontools/TTableHelper.cxx605
1 files changed, 605 insertions, 0 deletions
diff --git a/connectivity/source/commontools/TTableHelper.cxx b/connectivity/source/commontools/TTableHelper.cxx
new file mode 100644
index 000000000..03724d669
--- /dev/null
+++ b/connectivity/source/commontools/TTableHelper.cxx
@@ -0,0 +1,605 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <sal/config.h>
+#include <sal/log.hxx>
+
+#include <connectivity/TTableHelper.hxx>
+#include <com/sun/star/sdb/tools/XTableRename.hpp>
+#include <com/sun/star/sdb/tools/XTableAlteration.hpp>
+#include <com/sun/star/sdb/tools/XKeyAlteration.hpp>
+#include <com/sun/star/sdb/tools/XIndexAlteration.hpp>
+#include <sdbcx/VKey.hxx>
+#include <com/sun/star/sdbc/XRow.hpp>
+#include <com/sun/star/sdbc/XResultSet.hpp>
+#include <com/sun/star/sdbcx/KeyType.hpp>
+#include <cppuhelper/implbase.hxx>
+#include <com/sun/star/lang/XMultiServiceFactory.hpp>
+#include <comphelper/types.hxx>
+#include <connectivity/dbtools.hxx>
+#include <connectivity/sdbcx/VCollection.hxx>
+#include <unotools/sharedunocomponent.hxx>
+#include <TConnection.hxx>
+
+#include <o3tl/functional.hxx>
+
+#include <iterator>
+#include <set>
+
+using namespace ::comphelper;
+using namespace connectivity;
+using namespace ::com::sun::star::uno;
+using namespace ::com::sun::star::beans;
+using namespace ::com::sun::star::sdbcx;
+using namespace ::com::sun::star::sdbc;
+using namespace ::com::sun::star::container;
+using namespace ::com::sun::star::lang;
+
+namespace
+{
+ /// helper class for column property change events which holds the OComponentDefinition weak
+class OTableContainerListener:
+ public ::cppu::WeakImplHelper< XContainerListener >
+{
+ OTableHelper* m_pComponent;
+ std::map< OUString,bool> m_aRefNames;
+
+protected:
+ virtual ~OTableContainerListener() override {}
+public:
+ explicit OTableContainerListener(OTableHelper* _pComponent) : m_pComponent(_pComponent){}
+ // noncopyable
+ OTableContainerListener(const OTableContainerListener&) = delete;
+ const OTableContainerListener& operator=(const OTableContainerListener&) = delete;
+ virtual void SAL_CALL elementInserted( const css::container::ContainerEvent& /*Event*/ ) override
+ {
+ }
+ virtual void SAL_CALL elementRemoved( const css::container::ContainerEvent& Event ) override
+ {
+ // tdf#137745, perhaps connectivity::OTableHelper::disposing() has been called
+ // which called OTableContainerListener::clear(), so m_pComponent may be null
+ if (m_pComponent == nullptr)
+ return;
+
+ OUString sName;
+ Event.Accessor >>= sName;
+ if ( m_aRefNames.find(sName) != m_aRefNames.end() )
+ m_pComponent->refreshKeys();
+ }
+ virtual void SAL_CALL elementReplaced( const css::container::ContainerEvent& Event ) override
+ {
+ OUString sOldComposedName,sNewComposedName;
+ Event.ReplacedElement >>= sOldComposedName;
+ Event.Accessor >>= sNewComposedName;
+ if ( sOldComposedName != sNewComposedName && m_aRefNames.find(sOldComposedName) != m_aRefNames.end() )
+ m_pComponent->refreshKeys();
+ }
+ // XEventListener
+ virtual void SAL_CALL disposing( const EventObject& /*_rSource*/ ) override
+ {
+ }
+ void clear() { m_pComponent = nullptr; }
+ void add(const OUString& _sRefName) { m_aRefNames.emplace(_sRefName,true); }
+};
+}
+namespace connectivity
+{
+ static OUString lcl_getServiceNameForSetting(const Reference< css::sdbc::XConnection >& _xConnection,const OUString& i_sSetting)
+ {
+ OUString sSupportService;
+ Any aValue;
+ if ( ::dbtools::getDataSourceSetting(_xConnection,i_sSetting,aValue) )
+ {
+ aValue >>= sSupportService;
+ }
+ return sSupportService;
+ }
+ struct OTableHelperImpl
+ {
+ TKeyMap m_aKeys;
+ // helper services which can be provided by extensions
+ Reference< css::sdb::tools::XTableRename> m_xRename;
+ Reference< css::sdb::tools::XTableAlteration> m_xAlter;
+ Reference< css::sdb::tools::XKeyAlteration> m_xKeyAlter;
+ Reference< css::sdb::tools::XIndexAlteration> m_xIndexAlter;
+
+ Reference< css::sdbc::XDatabaseMetaData > m_xMetaData;
+ Reference< css::sdbc::XConnection > m_xConnection;
+ rtl::Reference<OTableContainerListener> m_xTablePropertyListener;
+ std::vector< ColumnDesc > m_aColumnDesc;
+ explicit OTableHelperImpl(const Reference< css::sdbc::XConnection >& _xConnection)
+ : m_xConnection(_xConnection)
+ {
+ try
+ {
+ m_xMetaData = m_xConnection->getMetaData();
+ Reference<XMultiServiceFactory> xFac(_xConnection,UNO_QUERY);
+ if ( xFac.is() )
+ {
+ m_xRename.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,"TableRenameServiceName")),UNO_QUERY);
+ m_xAlter.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,"TableAlterationServiceName")),UNO_QUERY);
+ m_xKeyAlter.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,"KeyAlterationServiceName")),UNO_QUERY);
+ m_xIndexAlter.set(xFac->createInstance(lcl_getServiceNameForSetting(m_xConnection,"IndexAlterationServiceName")),UNO_QUERY);
+ }
+ }
+ catch(const Exception&)
+ {
+ }
+ }
+ };
+}
+
+OTableHelper::OTableHelper( sdbcx::OCollection* _pTables,
+ const Reference< XConnection >& _xConnection,
+ bool _bCase)
+ :OTable_TYPEDEF(_pTables,_bCase)
+ ,m_pImpl(new OTableHelperImpl(_xConnection))
+{
+}
+
+OTableHelper::OTableHelper( sdbcx::OCollection* _pTables,
+ const Reference< XConnection >& _xConnection,
+ bool _bCase,
+ const OUString& Name,
+ const OUString& Type,
+ const OUString& Description ,
+ const OUString& SchemaName,
+ const OUString& CatalogName
+ ) : OTable_TYPEDEF(_pTables,
+ _bCase,
+ Name,
+ Type,
+ Description,
+ SchemaName,
+ CatalogName)
+ ,m_pImpl(new OTableHelperImpl(_xConnection))
+{
+}
+
+OTableHelper::~OTableHelper()
+{
+}
+
+void SAL_CALL OTableHelper::disposing()
+{
+ ::osl::MutexGuard aGuard(m_aMutex);
+ if ( m_pImpl->m_xTablePropertyListener.is() )
+ {
+ m_pTables->removeContainerListener(m_pImpl->m_xTablePropertyListener.get());
+ m_pImpl->m_xTablePropertyListener->clear();
+ m_pImpl->m_xTablePropertyListener.clear();
+ }
+ OTable_TYPEDEF::disposing();
+
+ m_pImpl->m_xConnection = nullptr;
+ m_pImpl->m_xMetaData = nullptr;
+
+}
+
+
+namespace
+{
+ /** collects ColumnDesc's from a resultset produced by XDatabaseMetaData::getColumns
+ */
+ void lcl_collectColumnDescs_throw( const Reference< XResultSet >& _rxResult, std::vector< ColumnDesc >& _out_rColumns )
+ {
+ Reference< XRow > xRow( _rxResult, UNO_QUERY_THROW );
+ OUString sName;
+ OrdinalPosition nOrdinalPosition( 0 );
+ while ( _rxResult->next() )
+ {
+ sName = xRow->getString( 4 ); // COLUMN_NAME
+ sal_Int32 nField5 = xRow->getInt(5);
+ OUString aField6 = xRow->getString(6);
+ sal_Int32 nField7 = xRow->getInt(7)
+ , nField9 = xRow->getInt(9)
+ , nField11= xRow->getInt(11);
+ OUString sField12 = xRow->getString(12)
+ ,sField13 = xRow->getString(13);
+ nOrdinalPosition = xRow->getInt( 17 ); // ORDINAL_POSITION
+ _out_rColumns.push_back( ColumnDesc( sName,nField5,aField6,nField7,nField9,nField11,sField12,sField13, nOrdinalPosition ) );
+ }
+ }
+
+ /** checks a given array of ColumnDesc's whether it has reasonable ordinal positions. If not,
+ they will be normalized to be the array index.
+ */
+ void lcl_sanitizeColumnDescs( std::vector< ColumnDesc >& _rColumns )
+ {
+ if ( _rColumns.empty() )
+ return;
+
+ // collect all used ordinals
+ std::set< OrdinalPosition > aUsedOrdinals;
+ for ( const auto& collect : _rColumns )
+ aUsedOrdinals.insert( collect.nOrdinalPosition );
+
+ // we need to have as much different ordinals as we have different columns
+ bool bDuplicates = aUsedOrdinals.size() != _rColumns.size();
+ // and it needs to be a continuous range
+ size_t nOrdinalsRange = *aUsedOrdinals.rbegin() - *aUsedOrdinals.begin() + 1;
+ bool bGaps = nOrdinalsRange != _rColumns.size();
+
+ // if that's not the case, normalize it
+ if ( bGaps || bDuplicates )
+ {
+ OSL_FAIL( "lcl_sanitizeColumnDescs: database did provide invalid ORDINAL_POSITION values!" );
+
+ OrdinalPosition nNormalizedPosition = 1;
+ for ( auto& normalize : _rColumns )
+ normalize.nOrdinalPosition = nNormalizedPosition++;
+ return;
+ }
+
+ // what's left is that the range might not be from 1 to <column count>, but for instance
+ // 0 to <column count>-1.
+ size_t nOffset = *aUsedOrdinals.begin() - 1;
+ for ( auto& offset : _rColumns )
+ offset.nOrdinalPosition -= nOffset;
+ }
+}
+
+
+void OTableHelper::refreshColumns()
+{
+ ::std::vector< OUString> aVector;
+ if(!isNew())
+ {
+ Any aCatalog;
+ if ( !m_CatalogName.isEmpty() )
+ aCatalog <<= m_CatalogName;
+
+ ::utl::SharedUNOComponent< XResultSet > xResult( getMetaData()->getColumns(
+ aCatalog,
+ m_SchemaName,
+ m_Name,
+ "%"
+ ) );
+
+ // collect the column names, together with their ordinal position
+ m_pImpl->m_aColumnDesc.clear();
+ lcl_collectColumnDescs_throw( xResult, m_pImpl->m_aColumnDesc );
+
+ // ensure that the ordinal positions as obtained from the meta data do make sense
+ lcl_sanitizeColumnDescs( m_pImpl->m_aColumnDesc );
+
+ // sort by ordinal position
+ std::map< OrdinalPosition, OUString > aSortedColumns;
+ for (const auto& copy : m_pImpl->m_aColumnDesc)
+ aSortedColumns[ copy.nOrdinalPosition ] = copy.sName;
+
+ // copy them to aVector, now that we have the proper ordering
+ std::transform(
+ aSortedColumns.begin(),
+ aSortedColumns.end(),
+ std::insert_iterator< ::std::vector< OUString> >( aVector, aVector.begin() ),
+ ::o3tl::select2nd< std::map< OrdinalPosition, OUString >::value_type >()
+ );
+ }
+
+ if(m_xColumns)
+ m_xColumns->reFill(aVector);
+ else
+ m_xColumns = createColumns(aVector);
+}
+
+const ColumnDesc* OTableHelper::getColumnDescription(const OUString& _sName) const
+{
+ const ColumnDesc* pRet = nullptr;
+ auto aIter = std::find_if(m_pImpl->m_aColumnDesc.begin(), m_pImpl->m_aColumnDesc.end(),
+ [&_sName](const ColumnDesc& rColumnDesc) { return rColumnDesc.sName == _sName; });
+ if (aIter != m_pImpl->m_aColumnDesc.end())
+ pRet = &*aIter;
+ return pRet;
+}
+
+void OTableHelper::refreshPrimaryKeys(::std::vector< OUString>& _rNames)
+{
+ Any aCatalog;
+ if ( !m_CatalogName.isEmpty() )
+ aCatalog <<= m_CatalogName;
+ Reference< XResultSet > xResult = getMetaData()->getPrimaryKeys(aCatalog,m_SchemaName,m_Name);
+
+ if ( xResult.is() )
+ {
+ auto pKeyProps = std::make_shared<sdbcx::KeyProperties>(OUString(),KeyType::PRIMARY,0,0);
+ OUString aPkName;
+ bool bAlreadyFetched = false;
+ const Reference< XRow > xRow(xResult,UNO_QUERY);
+ while ( xResult->next() )
+ {
+ pKeyProps->m_aKeyColumnNames.push_back(xRow->getString(4));
+ if ( !bAlreadyFetched )
+ {
+ aPkName = xRow->getString(6);
+ SAL_WARN_IF(xRow->wasNull(),"connectivity.commontools", "NULL Primary Key name");
+ SAL_WARN_IF(aPkName.isEmpty(),"connectivity.commontools", "empty Primary Key name");
+ bAlreadyFetched = true;
+ }
+ }
+
+ if(bAlreadyFetched)
+ {
+ SAL_WARN_IF(aPkName.isEmpty(),"connectivity.commontools", "empty Primary Key name");
+ SAL_WARN_IF(pKeyProps->m_aKeyColumnNames.empty(),"connectivity.commontools", "Primary Key has no columns");
+ m_pImpl->m_aKeys.emplace(aPkName,pKeyProps);
+ _rNames.push_back(aPkName);
+ }
+ } // if ( xResult.is() && xResult->next() )
+ ::comphelper::disposeComponent(xResult);
+}
+
+void OTableHelper::refreshForeignKeys(::std::vector< OUString>& _rNames)
+{
+ Any aCatalog;
+ if ( !m_CatalogName.isEmpty() )
+ aCatalog <<= m_CatalogName;
+ Reference< XResultSet > xResult = getMetaData()->getImportedKeys(aCatalog,m_SchemaName,m_Name);
+ Reference< XRow > xRow(xResult,UNO_QUERY);
+
+ if ( !xRow.is() )
+ return;
+
+ std::shared_ptr<sdbcx::KeyProperties> pKeyProps;
+ OUString aName,sCatalog,aSchema,sOldFKName;
+ while( xResult->next() )
+ {
+ // this must be outside the "if" because we have to call in a right order
+ sCatalog = xRow->getString(1);
+ if ( xRow->wasNull() )
+ sCatalog.clear();
+ aSchema = xRow->getString(2);
+ aName = xRow->getString(3);
+
+ const OUString sForeignKeyColumn = xRow->getString(8);
+ const sal_Int32 nUpdateRule = xRow->getInt(10);
+ const sal_Int32 nDeleteRule = xRow->getInt(11);
+ const OUString sFkName = xRow->getString(12);
+
+ if ( !sFkName.isEmpty() && !xRow->wasNull() )
+ {
+ if ( sOldFKName != sFkName )
+ {
+ if ( pKeyProps )
+ m_pImpl->m_aKeys.emplace(sOldFKName,pKeyProps);
+
+ const OUString sReferencedName = ::dbtools::composeTableName(getMetaData(),sCatalog,aSchema,aName,false,::dbtools::EComposeRule::InDataManipulation);
+ pKeyProps = std::make_shared<sdbcx::KeyProperties>(sReferencedName,KeyType::FOREIGN,nUpdateRule,nDeleteRule);
+ pKeyProps->m_aKeyColumnNames.push_back(sForeignKeyColumn);
+ _rNames.push_back(sFkName);
+ if ( m_pTables->hasByName(sReferencedName) )
+ {
+ if ( !m_pImpl->m_xTablePropertyListener.is() )
+ m_pImpl->m_xTablePropertyListener = new OTableContainerListener(this);
+ m_pTables->addContainerListener(m_pImpl->m_xTablePropertyListener.get());
+ m_pImpl->m_xTablePropertyListener->add(sReferencedName);
+ } // if ( m_pTables->hasByName(sReferencedName) )
+ sOldFKName = sFkName;
+ } // if ( sOldFKName != sFkName )
+ else if ( pKeyProps )
+ {
+ pKeyProps->m_aKeyColumnNames.push_back(sForeignKeyColumn);
+ }
+ }
+ } // while( xResult->next() )
+ if ( pKeyProps )
+ m_pImpl->m_aKeys.emplace(sOldFKName,pKeyProps);
+ ::comphelper::disposeComponent(xResult);
+}
+
+void OTableHelper::refreshKeys()
+{
+ m_pImpl->m_aKeys.clear();
+
+ ::std::vector< OUString> aNames;
+
+ if(!isNew())
+ {
+ refreshPrimaryKeys(aNames);
+ refreshForeignKeys(aNames);
+ m_xKeys = createKeys(aNames);
+ } // if(!isNew())
+ else if (!m_xKeys )
+ m_xKeys = createKeys(aNames);
+ /*if(m_pKeys)
+ m_pKeys->reFill(aVector);
+ else*/
+
+}
+
+void OTableHelper::refreshIndexes()
+{
+ ::std::vector< OUString> aVector;
+ if(!isNew())
+ {
+ // fill indexes
+ Any aCatalog;
+ if ( !m_CatalogName.isEmpty() )
+ aCatalog <<= m_CatalogName;
+ Reference< XResultSet > xResult = getMetaData()->getIndexInfo(aCatalog,m_SchemaName,m_Name,false,false);
+
+ if(xResult.is())
+ {
+ Reference< XRow > xRow(xResult,UNO_QUERY);
+ OUString sCatalogSep = getMetaData()->getCatalogSeparator();
+ OUString sPreviousRoundName;
+ while( xResult->next() )
+ {
+ OUString aName = xRow->getString(5);
+ if(!aName.isEmpty())
+ aName += sCatalogSep;
+ aName += xRow->getString(6);
+ if ( !aName.isEmpty() )
+ {
+ // don't insert the name if the last one we inserted was the same
+ if (sPreviousRoundName != aName)
+ aVector.push_back(aName);
+ }
+ sPreviousRoundName = aName;
+ }
+ ::comphelper::disposeComponent(xResult);
+ }
+ }
+
+ if(m_xIndexes)
+ m_xIndexes->reFill(aVector);
+ else
+ m_xIndexes = createIndexes(aVector);
+}
+
+OUString OTableHelper::getRenameStart() const
+{
+ OUString sSql("RENAME ");
+ if ( m_Type == "VIEW" )
+ sSql += " VIEW ";
+ else
+ sSql += " TABLE ";
+
+ return sSql;
+}
+
+// XRename
+void SAL_CALL OTableHelper::rename( const OUString& newName )
+{
+ ::osl::MutexGuard aGuard(m_aMutex);
+ checkDisposed(
+#ifdef __GNUC__
+ ::connectivity::sdbcx::OTableDescriptor_BASE::rBHelper.bDisposed
+#else
+ rBHelper.bDisposed
+#endif
+ );
+
+ if(!isNew())
+ {
+ if ( m_pImpl->m_xRename.is() )
+ {
+ m_pImpl->m_xRename->rename(this,newName);
+ }
+ else
+ {
+ OUString sSql = getRenameStart();
+
+ OUString sCatalog,sSchema,sTable;
+ ::dbtools::qualifiedNameComponents(getMetaData(),newName,sCatalog,sSchema,sTable,::dbtools::EComposeRule::InDataManipulation);
+
+ OUString sComposedName;
+ sComposedName = ::dbtools::composeTableName(getMetaData(),m_CatalogName,m_SchemaName,m_Name,true,::dbtools::EComposeRule::InDataManipulation);
+ sSql += sComposedName
+ + " TO ";
+ sComposedName = ::dbtools::composeTableName(getMetaData(),sCatalog,sSchema,sTable,true,::dbtools::EComposeRule::InDataManipulation);
+ sSql += sComposedName;
+
+ Reference< XStatement > xStmt = m_pImpl->m_xConnection->createStatement( );
+ if ( xStmt.is() )
+ {
+ xStmt->execute(sSql);
+ ::comphelper::disposeComponent(xStmt);
+ }
+ }
+
+ OTable_TYPEDEF::rename(newName);
+ }
+ else
+ ::dbtools::qualifiedNameComponents(getMetaData(),newName,m_CatalogName,m_SchemaName,m_Name,::dbtools::EComposeRule::InTableDefinitions);
+}
+
+Reference< XDatabaseMetaData> OTableHelper::getMetaData() const
+{
+ return m_pImpl->m_xMetaData;
+}
+
+void SAL_CALL OTableHelper::alterColumnByIndex( sal_Int32 index, const Reference< XPropertySet >& descriptor )
+{
+ ::osl::MutexGuard aGuard(m_aMutex);
+ checkDisposed(
+#ifdef __GNUC__
+ ::connectivity::sdbcx::OTableDescriptor_BASE::rBHelper.bDisposed
+#else
+ rBHelper.bDisposed
+#endif
+ );
+
+ Reference< XPropertySet > xOld(
+ m_xColumns->getByIndex(index), css::uno::UNO_QUERY);
+ if(xOld.is())
+ alterColumnByName(getString(xOld->getPropertyValue(OMetaConnection::getPropMap().getNameByIndex(PROPERTY_ID_NAME))),descriptor);
+}
+
+
+OUString SAL_CALL OTableHelper::getName()
+{
+ OUString sComposedName = ::dbtools::composeTableName(getMetaData(),m_CatalogName,m_SchemaName,m_Name,false,::dbtools::EComposeRule::InDataManipulation);
+ return sComposedName;
+}
+
+std::shared_ptr<sdbcx::KeyProperties> OTableHelper::getKeyProperties(const OUString& _sName) const
+{
+ std::shared_ptr<sdbcx::KeyProperties> pKeyProps;
+ TKeyMap::const_iterator aFind = m_pImpl->m_aKeys.find(_sName);
+ if ( aFind != m_pImpl->m_aKeys.end() )
+ {
+ pKeyProps = aFind->second;
+ }
+ else // only a fall back
+ {
+ OSL_FAIL("No key with the given name found");
+ pKeyProps = std::make_shared<sdbcx::KeyProperties>();
+ }
+
+ return pKeyProps;
+}
+
+void OTableHelper::addKey(const OUString& _sName,const std::shared_ptr<sdbcx::KeyProperties>& _aKeyProperties)
+{
+ m_pImpl->m_aKeys.emplace(_sName,_aKeyProperties);
+}
+
+OUString OTableHelper::getTypeCreatePattern() const
+{
+ return OUString();
+}
+
+Reference< XConnection> const & OTableHelper::getConnection() const
+{
+ return m_pImpl->m_xConnection;
+}
+
+Reference< css::sdb::tools::XTableRename> const & OTableHelper::getRenameService() const
+{
+ return m_pImpl->m_xRename;
+}
+
+Reference< css::sdb::tools::XTableAlteration> const & OTableHelper::getAlterService() const
+{
+ return m_pImpl->m_xAlter;
+}
+
+Reference< css::sdb::tools::XKeyAlteration> const & OTableHelper::getKeyService() const
+{
+ return m_pImpl->m_xKeyAlter;
+}
+
+Reference< css::sdb::tools::XIndexAlteration> const & OTableHelper::getIndexService() const
+{
+ return m_pImpl->m_xIndexAlter;
+}
+
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */