/* -*- 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 "ucpext_content.hxx"
#include "ucpext_provider.hxx"
#include "ucpext_resultset.hxx"
 
#include <com/sun/star/beans/PropertyAttribute.hpp>
#include <com/sun/star/beans/XPropertySetInfo.hpp>
#include <com/sun/star/lang/IllegalAccessException.hpp>
#include <com/sun/star/sdbc/XRow.hpp>
#include <com/sun/star/ucb/XCommandInfo.hpp>
#include <com/sun/star/ucb/OpenCommandArgument2.hpp>
#include <com/sun/star/ucb/OpenMode.hpp>
#include <com/sun/star/ucb/UnsupportedCommandException.hpp>
#include <com/sun/star/ucb/XDynamicResultSet.hpp>
#include <com/sun/star/deployment/PackageInformationProvider.hpp>
 
#include <o3tl/string_view.hxx>
#include <ucbhelper/propertyvalueset.hxx>
#include <ucbhelper/cancelcommandexecution.hxx>
#include <ucbhelper/content.hxx>
#include <comphelper/diagnose_ex.hxx>
#include <rtl/ustrbuf.hxx>
#include <rtl/uri.hxx>
#include <sal/macros.h>
#include <sal/log.hxx>
 
#include <algorithm>
#include <string_view>
 
 
namespace ucb::ucp::ext
{
 
 
    using ::com::sun::star::uno::Reference;
    using ::com::sun::star::uno::UNO_SET_THROW;
    using ::com::sun::star::uno::Exception;
    using ::com::sun::star::uno::Any;
    using ::com::sun::star::uno::Sequence;
    using ::com::sun::star::uno::XComponentContext;
    using ::com::sun::star::ucb::XContentIdentifier;
    using ::com::sun::star::ucb::XCommandEnvironment;
    using ::com::sun::star::ucb::Command;
    using ::com::sun::star::beans::Property;
    using ::com::sun::star::lang::IllegalArgumentException;
    using ::com::sun::star::beans::PropertyValue;
    using ::com::sun::star::ucb::OpenCommandArgument2;
    using ::com::sun::star::ucb::XDynamicResultSet;
    using ::com::sun::star::ucb::UnsupportedCommandException;
    using ::com::sun::star::sdbc::XRow;
    using ::com::sun::star::beans::PropertyChangeEvent;
    using ::com::sun::star::lang::IllegalAccessException;
    using ::com::sun::star::ucb::CommandInfo;
    using ::com::sun::star::deployment::PackageInformationProvider;
    using ::com::sun::star::deployment::XPackageInformationProvider;
 
    namespace OpenMode = css::ucb::OpenMode;
    namespace PropertyAttribute = css::beans::PropertyAttribute;
 
 
    //= helper
 
    namespace
    {
 
        OUString lcl_compose( std::u16string_view i_rBaseURL, const OUString& i_rRelativeURL )
        {
            ENSURE_OR_RETURN( !i_rBaseURL.empty(), "illegal base URL", i_rRelativeURL );
 
            OUStringBuffer aComposer( i_rBaseURL );
            if ( !o3tl::ends_with(i_rBaseURL, u"/") )
                aComposer.append( '/' );
            aComposer.append( i_rRelativeURL );
            return aComposer.makeStringAndClear();
        }
 
 
        struct SelectPropertyName
        {
            const OUString& operator()( const Property& i_rProperty ) const
            {
                return i_rProperty.Name;
            }
        };
    }
 
 
    //= Content
 
 
    Content::Content( const Reference< XComponentContext >& rxContext, ::ucbhelper::ContentProviderImplHelper* i_pProvider,
                      const Reference< XContentIdentifier >& i_rIdentifier )
        :Content_Base( rxContext, i_pProvider, i_rIdentifier )
        ,m_eExtContentType( E_UNKNOWN )
    {
        const OUString sURL( getIdentifier()->getContentIdentifier() );
        if ( denotesRootContent( sURL ) )
        {
            m_eExtContentType = E_ROOT;
        }
        else
        {
            const std::u16string_view sRelativeURL( sURL.subView( ContentProvider::getRootURL().getLength() ) );
            const size_t nSepPos = sRelativeURL.find( '/' );
            if ( ( nSepPos == std::u16string_view::npos ) || ( nSepPos == sRelativeURL.size() - 1 ) )
            {
                m_eExtContentType = E_EXTENSION_ROOT;
            }
            else
            {
                m_eExtContentType = E_EXTENSION_CONTENT;
            }
        }
 
        if ( m_eExtContentType == E_ROOT )
            return;
 
        const OUString sRootURL = ContentProvider::getRootURL();
        m_sExtensionId = sURL.copy( sRootURL.getLength() );
 
        const sal_Int32 nNextSep = m_sExtensionId.indexOf( '/' );
        if ( nNextSep > -1 )
        {
            m_sPathIntoExtension = m_sExtensionId.copy( nNextSep + 1 );
            m_sExtensionId = m_sExtensionId.copy( 0, nNextSep );
        }
        m_sExtensionId = Content::decodeIdentifier( m_sExtensionId );
    }
 
 
    Content::~Content()
    {
    }
 
 
    OUString SAL_CALL Content::getImplementationName()
    {
        return u"org.openoffice.comp.ucp.ext.Content"_ustr;
    }
 
 
    Sequence< OUString > SAL_CALL Content::getSupportedServiceNames()
    {
        return { u"com.sun.star.ucb.Content"_ustr, u"com.sun.star.ucb.ExtensionContent"_ustr };
    }
 
 
    OUString SAL_CALL Content::getContentType()
    {
        impl_determineContentType();
        return *m_aContentType;
    }
 
 
    Any SAL_CALL Content::execute( const Command& aCommand, sal_Int32 /* CommandId */, const Reference< XCommandEnvironment >& i_rEnvironment )
    {
        Any aRet;
 
        if ( aCommand.Name == "getPropertyValues" )
        {
            Sequence< Property > Properties;
            if ( !( aCommand.Argument >>= Properties ) )
            {
                ::ucbhelper::cancelCommandExecution( Any( IllegalArgumentException(
                    OUString(), *this, -1 ) ),
                    i_rEnvironment );
                // unreachable
            }
 
            aRet <<= getPropertyValues( Properties, i_rEnvironment );
        }
        else if ( aCommand.Name == "setPropertyValues" )
        {
            Sequence< PropertyValue > aProperties;
            if ( !( aCommand.Argument >>= aProperties ) )
            {
                ::ucbhelper::cancelCommandExecution( Any( IllegalArgumentException(
                    OUString(), *this, -1 ) ),
                    i_rEnvironment );
                // unreachable
            }
 
            if ( !aProperties.hasElements() )
            {
                ::ucbhelper::cancelCommandExecution( Any( IllegalArgumentException(
                    OUString(), *this, -1 ) ),
                    i_rEnvironment );
                // unreachable
            }
 
            aRet <<= setPropertyValues( aProperties );
        }
        else if ( aCommand.Name == "getPropertySetInfo" )
        {
            // implemented by base class.
            aRet <<= getPropertySetInfo( i_rEnvironment );
        }
        else if ( aCommand.Name == "getCommandInfo" )
        {
            // implemented by base class.
            aRet <<= getCommandInfo( i_rEnvironment );
        }
        else if ( aCommand.Name == "open" )
        {
            OpenCommandArgument2 aOpenCommand;
            if ( !( aCommand.Argument >>= aOpenCommand ) )
            {
                ::ucbhelper::cancelCommandExecution( Any( IllegalArgumentException(
                    OUString(), *this, -1 ) ),
                    i_rEnvironment );
                // unreachable
            }
 
            bool bOpenFolder =
                ( ( aOpenCommand.Mode == OpenMode::ALL ) ||
                  ( aOpenCommand.Mode == OpenMode::FOLDERS ) ||
                  ( aOpenCommand.Mode == OpenMode::DOCUMENTS ) );
 
 
            if ( bOpenFolder && impl_isFolder() )
            {
                Reference< XDynamicResultSet > xSet = new ResultSet( m_xContext, this, aOpenCommand, i_rEnvironment );
                aRet <<= xSet;
            }
 
            if ( aOpenCommand.Sink.is() )
            {
                const OUString sPhysicalContentURL( getPhysicalURL() );
                ::ucbhelper::Content aRequestedContent( sPhysicalContentURL, i_rEnvironment, m_xContext );
                aRet = aRequestedContent.executeCommand( u"open"_ustr, Any( aOpenCommand ) );
            }
        }
 
        else
        {
            ::ucbhelper::cancelCommandExecution( Any( UnsupportedCommandException(
                OUString(), *this ) ),
                i_rEnvironment );
            // unreachable
        }
 
        return aRet;
    }
 
 
    void SAL_CALL Content::abort( sal_Int32 )
    {
    }
 
 
    OUString Content::encodeIdentifier( const OUString& i_rIdentifier )
    {
        return ::rtl::Uri::encode( i_rIdentifier, rtl_UriCharClassRegName, rtl_UriEncodeIgnoreEscapes,
            RTL_TEXTENCODING_UTF8 );
    }
 
 
    OUString Content::decodeIdentifier( const OUString& i_rIdentifier )
    {
        return ::rtl::Uri::decode( i_rIdentifier, rtl_UriDecodeWithCharset, RTL_TEXTENCODING_UTF8 );
    }
 
 
    bool Content::denotesRootContent( std::u16string_view i_rContentIdentifier )
    {
        const OUString sRootURL( ContentProvider::getRootURL() );
        if ( i_rContentIdentifier == sRootURL )
            return true;
 
        // the root URL contains only two trailing /, but we also recognize 3 of them as denoting the root URL
        if  (   o3tl::starts_with(i_rContentIdentifier,  sRootURL )
            &&  ( sal_Int32(i_rContentIdentifier.size()) == sRootURL.getLength() + 1 )
            &&  ( i_rContentIdentifier[ i_rContentIdentifier.size() - 1 ] == '/' )
            )
            return true;
 
        return false;
    }
 
 
    OUString Content::getParentURL()
    {
        const OUString sRootURL( ContentProvider::getRootURL() );
 
        switch ( m_eExtContentType )
        {
        case E_ROOT:
            // don't have a parent
            return sRootURL;
 
        case E_EXTENSION_ROOT:
            // our parent is the root itself
            return sRootURL;
 
        case E_EXTENSION_CONTENT:
        {
            const OUString sURL = m_xIdentifier->getContentIdentifier();
 
            // cut the root URL
            if ( !sURL.match( sRootURL ) )
            {
                SAL_INFO( "ucb.ucp.ext", "illegal URL structure - no root" );
                break;
            }
 
            OUString sRelativeURL( sURL.copy( sRootURL.getLength() ) );
 
            // cut the extension ID
            const OUString sSeparatedExtensionId( encodeIdentifier( m_sExtensionId ) + "/" );
            if ( !sRelativeURL.match( sSeparatedExtensionId ) )
            {
                SAL_INFO( "ucb.ucp.ext", "illegal URL structure - no extension ID" );
                break;
            }
 
            sRelativeURL = sRelativeURL.copy( sSeparatedExtensionId.getLength() );
 
            // cut the final slash (if any)
            if ( sRelativeURL.isEmpty() )
            {
                SAL_INFO( "ucb.ucp.ext", "illegal URL structure - ExtensionContent should have a level below the extension ID" );
                break;
            }
 
            if ( sRelativeURL.endsWith("/") )
                sRelativeURL = sRelativeURL.copy( 0, sRelativeURL.getLength() - 1 );
 
            // remove the last segment
            const sal_Int32 nLastSep = sRelativeURL.lastIndexOf( '/' );
            sRelativeURL = sRelativeURL.copy( 0, nLastSep != -1 ? nLastSep : 0 );
 
            return sRootURL + sSeparatedExtensionId + sRelativeURL;
        }
 
        default:
            OSL_FAIL( "Content::getParentURL: unhandled case!" );
            break;
        }
        return OUString();
    }
 
 
    Reference< XRow > Content::getArtificialNodePropertyValues( const Reference< XComponentContext >& rxContext,
        const Sequence< Property >& i_rProperties, const OUString& i_rTitle )
    {
        // note: empty sequence means "get values of all supported properties".
        ::rtl::Reference< ::ucbhelper::PropertyValueSet > xRow = new ::ucbhelper::PropertyValueSet( rxContext );
 
        if ( i_rProperties.hasElements() )
        {
            for ( const Property& rProp : i_rProperties )
            {
                // Process Core properties.
                if ( rProp.Name == "ContentType" )
                {
                    xRow->appendString ( rProp, ContentProvider::getArtificialNodeContentType() );
                }
                else if ( rProp.Name == "Title" )
                {
                    xRow->appendString ( rProp, i_rTitle );
                }
                else if ( rProp.Name == "IsDocument" )
                {
                    xRow->appendBoolean( rProp, false );
                }
                else if ( rProp.Name == "IsFolder" )
                {
                    xRow->appendBoolean( rProp, true );
                }
                else
                {
                    // append empty entry.
                    xRow->appendVoid( rProp );
                }
            }
        }
        else
        {
            // Append all Core Properties.
            xRow->appendString ( Property( u"ContentType"_ustr,
                          -1,
                          cppu::UnoType<OUString>::get(),
                          PropertyAttribute::BOUND | PropertyAttribute::READONLY ),
                ContentProvider::getArtificialNodeContentType() );
            xRow->appendString ( Property( u"Title"_ustr,
                          -1,
                          cppu::UnoType<OUString>::get(),
                          PropertyAttribute::BOUND | PropertyAttribute::READONLY ),
                i_rTitle );
            xRow->appendBoolean( Property( u"IsDocument"_ustr,
                          -1,
                          cppu::UnoType<bool>::get(),
                          PropertyAttribute::BOUND | PropertyAttribute::READONLY ),
                false );
            xRow->appendBoolean( Property( u"IsFolder"_ustr,
                          -1,
                          cppu::UnoType<bool>::get(),
                          PropertyAttribute::BOUND | PropertyAttribute::READONLY ),
                true );
        }
 
        return xRow;
    }
 
 
    OUString Content::getPhysicalURL() const
    {
        ENSURE_OR_RETURN( m_eExtContentType != E_ROOT, "illegal call", OUString() );
 
        // create a ucb::XContent for the physical file within the deployed extension
        const Reference< XPackageInformationProvider > xPackageInfo = PackageInformationProvider::get(m_xContext);
        const OUString sPackageLocation( xPackageInfo->getPackageLocation( m_sExtensionId ) );
 
        if ( m_sPathIntoExtension.isEmpty() )
            return sPackageLocation;
        return lcl_compose( sPackageLocation, m_sPathIntoExtension );
    }
 
 
    Reference< XRow > Content::getPropertyValues( const Sequence< Property >& i_rProperties, const Reference< XCommandEnvironment >& i_rEnv )
    {
        ::osl::Guard< ::osl::Mutex > aGuard( m_aMutex );
 
        switch ( m_eExtContentType )
        {
        case E_ROOT:
            return getArtificialNodePropertyValues( m_xContext, i_rProperties, ContentProvider::getRootURL() );
        case E_EXTENSION_ROOT:
            return getArtificialNodePropertyValues( m_xContext, i_rProperties, m_sExtensionId );
        case E_EXTENSION_CONTENT:
        {
            const OUString sPhysicalContentURL( getPhysicalURL() );
            ::ucbhelper::Content aRequestedContent( sPhysicalContentURL, i_rEnv, m_xContext );
 
            // translate the property request
            Sequence< OUString > aPropertyNames( i_rProperties.getLength() );
            ::std::transform(
                i_rProperties.begin(),
                i_rProperties.end(),
                aPropertyNames.getArray(),
                SelectPropertyName()
            );
            const Sequence< Any > aPropertyValues = aRequestedContent.getPropertyValues( aPropertyNames );
            const ::rtl::Reference< ::ucbhelper::PropertyValueSet > xValueRow = new ::ucbhelper::PropertyValueSet( m_xContext );
            sal_Int32 i=0;
            for (   const Any* value = aPropertyValues.getConstArray();
                    value != aPropertyValues.getConstArray() + aPropertyValues.getLength();
                    ++value, ++i
                )
            {
                xValueRow->appendObject( aPropertyNames[i], *value );
            }
            return xValueRow;
        }
 
        default:
            OSL_FAIL( "Content::getPropertyValues: unhandled case!" );
            break;
        }
 
        OSL_FAIL( "Content::getPropertyValues: unreachable!" );
        return nullptr;
    }
 
 
    Sequence< Any > Content::setPropertyValues( const Sequence< PropertyValue >& i_rValues)
    {
        ::osl::ClearableGuard< osl::Mutex > aGuard( m_aMutex );
 
        Sequence< Any > aRet( i_rValues.getLength() );
 
        PropertyChangeEvent aEvent;
        aEvent.Source         = getXWeak();
        aEvent.Further        = false;
        aEvent.PropertyHandle = -1;
 
        for ( auto& rRet : asNonConstRange(aRet) )
        {
            // all our properties are read-only ...
            rRet <<= IllegalAccessException(u"property is read-only."_ustr, *this );
        }
 
        return aRet;
    }
 
 
    Sequence< CommandInfo > Content::getCommands( const Reference< XCommandEnvironment > & /*xEnv*/ )
    {
        static const CommandInfo aCommandInfoTable[] =
        {
            // Mandatory commands
 
            CommandInfo(
                u"getCommandInfo"_ustr,
                -1,
                cppu::UnoType<void>::get()
            ),
            CommandInfo(
                u"getPropertySetInfo"_ustr,
                -1,
                cppu::UnoType<void>::get()
            ),
            CommandInfo(
                u"getPropertyValues"_ustr,
                -1,
                cppu::UnoType<Sequence< Property >>::get()
            ),
            CommandInfo(
                u"setPropertyValues"_ustr,
                -1,
                cppu::UnoType<Sequence< PropertyValue >>::get()
            )
 
            // Optional standard commands
 
            , CommandInfo(
                u"open"_ustr,
                -1,
                cppu::UnoType<OpenCommandArgument2>::get()
            )
        };
 
        return Sequence< CommandInfo >( aCommandInfoTable, SAL_N_ELEMENTS(aCommandInfoTable) );
    }
 
 
    Sequence< Property > Content::getProperties( const Reference< XCommandEnvironment > & /*xEnv*/ )
    {
        static const Property aProperties[] =
        {
            Property(
                u"ContentType"_ustr,
                -1,
                cppu::UnoType<OUString>::get(),
                PropertyAttribute::BOUND | PropertyAttribute::READONLY
            ),
            Property(
                u"IsDocument"_ustr,
                -1,
                cppu::UnoType<bool>::get(),
                PropertyAttribute::BOUND | PropertyAttribute::READONLY
            ),
            Property(
                u"IsFolder"_ustr,
                -1,
                cppu::UnoType<bool>::get(),
                PropertyAttribute::BOUND | PropertyAttribute::READONLY
            ),
            Property(
                u"Title"_ustr,
                -1,
                cppu::UnoType<OUString>::get(),
                PropertyAttribute::BOUND | PropertyAttribute::READONLY
            )
        };
        return Sequence< Property >( aProperties, SAL_N_ELEMENTS( aProperties ) );
    }
 
 
    bool Content::impl_isFolder()
    {
        if ( m_aIsFolder.has_value() )
            return *m_aIsFolder;
 
        bool bIsFolder = false;
        try
        {
            Sequence< Property > aProps{ { /*Name*/ u"IsFolder"_ustr, {}, {}, {} } };
            Reference< XRow > xRow( getPropertyValues( aProps, nullptr ), UNO_SET_THROW );
            bIsFolder = xRow->getBoolean(1);
        }
        catch( const Exception& )
        {
            DBG_UNHANDLED_EXCEPTION("ucb.ucp.ext");
        }
        m_aIsFolder = bIsFolder;
        return *m_aIsFolder;
    }
 
 
    void Content::impl_determineContentType()
    {
        if ( !!m_aContentType )
            return;
 
        m_aContentType = ContentProvider::getArtificialNodeContentType();
        if ( m_eExtContentType != E_EXTENSION_CONTENT )
            return;
 
        try
        {
            Sequence< Property > aProps{ { /*Name*/ u"ContentType"_ustr, {}, {}, {} } };
            Reference< XRow > xRow( getPropertyValues( aProps, nullptr ), UNO_SET_THROW );
            m_aContentType = xRow->getString(1);
        }
        catch( const Exception& )
        {
            DBG_UNHANDLED_EXCEPTION("ucb.ucp.ext");
        }
    }
 
 
}   // namespace ucp::ext
 
 
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

V530 The return value of function 'append' is required to be utilized.

V1053 Calling the 'getIdentifier' virtual function in the constructor may lead to unexpected result at runtime.

V547 Expression '!i_rBaseURL.empty()' is always false.