UnitService.java

/*
 * Copyright (c) 2002-2021, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.unittree.service.unit;

import fr.paris.lutece.api.user.User;
import fr.paris.lutece.plugins.unittree.business.action.IAction;
import fr.paris.lutece.plugins.unittree.business.unit.TreeUnit;
import fr.paris.lutece.plugins.unittree.business.unit.Unit;
import fr.paris.lutece.plugins.unittree.business.unit.UnitFilter;
import fr.paris.lutece.plugins.unittree.business.unit.UnitHome;
import fr.paris.lutece.plugins.unittree.service.UnitErrorException;
import fr.paris.lutece.plugins.unittree.service.action.IActionService;
import fr.paris.lutece.plugins.unittree.service.rbac.UnittreeRBACRecursiveType;
import fr.paris.lutece.portal.business.user.AdminUser;
import fr.paris.lutece.portal.service.i18n.I18nService;
import fr.paris.lutece.portal.service.rbac.RBACService;
import fr.paris.lutece.portal.service.util.AppPathService;
import fr.paris.lutece.util.ReferenceList;
import fr.paris.lutece.util.xml.XmlUtil;

import org.apache.commons.lang3.StringUtils;

import org.springframework.transaction.annotation.Transactional;

import java.io.FileInputStream;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;

import javax.servlet.http.HttpServletRequest;

import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

/**
 *
 * UnitService
 *
 */
public class UnitService implements IUnitService
{
    public static final String BEAN_UNIT_SERVICE = "unittree.unitService";

    // XML TAGS
    private static final String TAG_UNITS = "units";
    private static final String TAG_UNIT = "unit";
    private static final String TAG_ID_UNIT = "id-unit";
    private static final String TAG_LABEL = "label";
    private static final String TAG_DESCRIPTION = "description";
    private static final String TAG_UNIT_CHILDREN = "unit-children";
    private static final String CDATA_START = "<![CDATA[";
    private static final String CDATA_END = "]]>";

    // PROPERTIES
    private static final String PROPERTY_LABEL_PARENT_UNIT = "unittree.moveUser.labelParentUnit";

    // CONSTANTS
    private static final String ATTRIBUTE_ID_UNIT = "idUnit";
    private static final String ATTRIBUTE_LABEL = "label";

    // XSL
    private static final String PATH_XSL = "/WEB-INF/plugins/unittree/xsl/";
    private static final String FILE_TREE_XSL = "units_tree.xsl";
    @Inject
    private IUnitUserService _unitUserService;
    @Inject
    private IActionService _actionService;

    // GET

    /**
     * {@inheritDoc}
     */
    @Override
    public Unit getUnit( int nIdUnit, boolean bGetAdditionalInfos )
    {
        Unit unit = UnitHome.findByPrimaryKey( nIdUnit );

        if ( ( unit != null ) && bGetAdditionalInfos )
        {
            UnitAttributeManager.populate( unit );
        }

        return unit;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Unit getRootUnit( boolean bGetAdditionalInfos )
    {
        return getUnit( Unit.ID_ROOT, bGetAdditionalInfos );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getUnitsByIdUser( int nIdUser, boolean bGetAdditionalInfos )
    {
        List<Unit> listUnit = UnitHome.findByIdUser( nIdUser );

        for ( Unit unit : listUnit )
        {
            if ( bGetAdditionalInfos && ( unit != null ) )
            {
                UnitAttributeManager.populate( unit );
            }
        }

        return listUnit;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getAllUnits( boolean bGetAdditionalInfos )
    {
        List<Unit> listUnits = UnitHome.findAll( );

        if ( bGetAdditionalInfos )
        {
            for ( Unit unit : listUnits )
            {
                if ( unit != null )
                {
                    UnitAttributeManager.populate( unit );
                }
            }
        }

        return listUnits;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getUnitsFirstLevel( boolean bGetAdditionalInfos )
    {
        return getSubUnits( Unit.ID_ROOT, bGetAdditionalInfos );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getSubUnits( int nIdUnit, boolean bGetAdditionalInfos )
    {
        // If the ID unit == -1, then only return the root unit
        if ( nIdUnit == Unit.ID_NULL )
        {
            List<Unit> listUnits = new ArrayList<>( );
            listUnits.add( getRootUnit( bGetAdditionalInfos ) );

            return listUnits;
        }

        UnitFilter uFilter = new UnitFilter( );
        uFilter.setIdParent( nIdUnit );

        List<Unit> listUnits = UnitHome.findByFilter( uFilter );

        if ( bGetAdditionalInfos )
        {
            for ( Unit unit : listUnits )
            {
                if ( unit != null )
                {
                    UnitAttributeManager.populate( unit );
                }
            }
        }

        return listUnits;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getAllSubUnits( Unit unit, boolean bGetAdditionalInfos )
    {
        List<Unit> listSubUnits = getSubUnits( unit.getIdUnit( ), bGetAdditionalInfos );

        return getAllSubUnitsRecursive( listSubUnits, bGetAdditionalInfos );
    }

    /**
     * Recursive method to get all the sub units from a list of units
     * 
     * @param listUnits
     *            the list of units the method has to search the sub units
     * @param bGetAdditionalInfos
     *            {@code true} if it must get the additionnal infos, {@code false} otherwise
     * @return all the sub units
     */
    private List<Unit> getAllSubUnitsRecursive( List<Unit> listUnits, boolean bGetAdditionalInfos )
    {
        List<Unit> listCurrentUnitsAndSubUnits = new ArrayList<>( listUnits );

        for ( Unit unit : listUnits )
        {
            List<Unit> listSubUnits = getSubUnits( unit.getIdUnit( ), bGetAdditionalInfos );
            listCurrentUnitsAndSubUnits.addAll( getAllSubUnitsRecursive( listSubUnits, bGetAdditionalInfos ) );
        }

        return listCurrentUnitsAndSubUnits;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<IAction> getListActions( String strActionType, Locale locale, Unit unit, AdminUser user, UnittreeRBACRecursiveType recursiveType )
    {
        List<IAction> listActions = _actionService.getListActions( strActionType, locale, unit, user, strActionType );
        listActions = getAuthorizedActions( listActions, unit, user, recursiveType );

        // If the unit can't be created, then remove 'CREATE' action
        if ( unit != null && !canCreateSubUnit( unit.getIdUnit( ) ) )
        {
            Integer nIndexToRemove = null;
            for ( int i = 0; i < listActions.size( ); i++ )
            {
                if ( listActions.get( i ).getPermission( ).equals( UnitResourceIdService.PERMISSION_CREATE ) )
                {
                    nIndexToRemove = i;
                }
            }
            if ( nIndexToRemove != null )
            {
                listActions.remove( nIndexToRemove.intValue( ) );
            }
        }

        return listActions;
    }

    /**
     * Filter a list of RBACAction for a given user
     *
     * @param listActions
     *            The list of actions
     * @param unit
     *            The unit linked to the actions
     * @param user
     *            The user
     * @param recursiveType
     *            the recursive type
     * @return The filtered list
     */
    private List<IAction> getAuthorizedActions( List<IAction> listActions, Unit unit, AdminUser user, UnittreeRBACRecursiveType recursiveType )
    {
        List<IAction> listAthorizedActions = new ArrayList<>( );

        for ( IAction action : listActions )
        {
            if ( isAuthorized( unit, action.getPermission( ), user, recursiveType ) )
            {
                listAthorizedActions.add( action );
            }
        }

        return listAthorizedActions;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ReferenceList getSubUnitsAsReferenceList( int nIdUnit, Locale locale )
    {
        // If parent id == -1, then its sub units is the root unit
        if ( nIdUnit == Unit.ID_NULL )
        {
            ReferenceList listSubUnits = new ReferenceList( );
            listSubUnits.addItem( Unit.ID_ROOT, getRootUnit( false ).getLabel( ) );

            return listSubUnits;
        }

        // Otherwise, build the reference list
        Unit unit = getUnit( nIdUnit, false );

        if ( unit != null )
        {
            String strLabelParentUnit = I18nService.getLocalizedString( PROPERTY_LABEL_PARENT_UNIT, locale );
            ReferenceList listSubUnits = new ReferenceList( );
            listSubUnits.addItem( unit.getIdParent( ), strLabelParentUnit );
            listSubUnits.addAll( ReferenceList.convert( getSubUnits( nIdUnit, false ), ATTRIBUTE_ID_UNIT, ATTRIBUTE_LABEL, true ) );

            return listSubUnits;
        }

        return new ReferenceList( );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void populateTreeUnit( TreeUnit treeUnit, AdminUser user, boolean bGetAdditionnalInfos )
    {
        if ( treeUnit == null || treeUnit.getUnitNode( ) == null )
        {
            return;
        }

        List<Unit> subUnits = getSubUnits( treeUnit.getUnitNode( ).getIdUnit( ), bGetAdditionnalInfos );

        // set sub units
        for ( Unit subUnit : subUnits )
        {
            if ( isAuthorized( subUnit, UnitResourceIdService.PERMISSION_SEE_UNIT, user, UnittreeRBACRecursiveType.NOT_RECURSIVE ) )
            {
                treeUnit.addSubUnit( subUnit );
            }
        }

        // recursive search to get the complete tree
        for ( TreeUnit subTreeUnit : treeUnit.getSubUnits( ) )
        {
            populateTreeUnit( subTreeUnit, user, bGetAdditionnalInfos );
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getXMLUnits( AdminUser user )
    {
        List<Unit> listAuthorizedUnit = getTreeableAuthorizedUnits( user );

        StringBuffer sbXML = new StringBuffer( );
        XmlUtil.beginElement( sbXML, TAG_UNITS );

        Unit rootUnit = getRootUnit( false );

        if ( isUnitInList( rootUnit, listAuthorizedUnit ) )
        {
            getXMLUnit( sbXML, listAuthorizedUnit, rootUnit, user );
        }

        XmlUtil.endElement( sbXML, TAG_UNITS );

        return sbXML.toString( );
    }

    /**
     * Get the authorized unit lists for the given user. The list is computed to be represented as a tree
     * 
     * @param user
     *            the lutece AdminUser
     * @return the list of authorized units, which can be represented as a tree.
     */
    private List<Unit> getTreeableAuthorizedUnits( AdminUser user )
    {
        List<Unit> listAllUnits = UnitHome.findAll( );
        Set<Unit> setAuthorizedUnits = new HashSet<>( );

        for ( Unit unit : listAllUnits )
        {
            if ( !isUnitInList( unit, setAuthorizedUnits )
                    && isAuthorized( unit, UnitResourceIdService.PERMISSION_SEE_UNIT, user, UnittreeRBACRecursiveType.NOT_RECURSIVE ) )
            {
                List<Unit> listAllSubUnits = getAllSubUnits( unit, false );

                for ( Unit childUnit : listAllSubUnits )
                {
                    setAuthorizedUnits.add( childUnit );
                }

                List<Unit> parentUnits = getListParentUnits( unit );

                for ( Unit parentUnit : parentUnits )
                {
                    setAuthorizedUnits.add( parentUnit );
                }
            }
        }

        return new ArrayList<>( setAuthorizedUnits );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Source getTreeXsl( )
    {
        FileInputStream fis = AppPathService.getResourceAsStream( PATH_XSL, FILE_TREE_XSL );

        return new StreamSource( fis );
    }

    /**
     * Return all the Unit with no children (level 0)
     * 
     * @return all the Unit with no children (level 0)
     */
    public List<Unit> getUnitWithNoChildren( )
    {
        return UnitHome.getUnitWithNoChildren( );
    }

    // CHECKS

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasSubUnits( int nIdUnit )
    {
        return UnitHome.hasSubUnits( nIdUnit );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isParent( Unit unitParent, Unit unitRef )
    {
        if ( ( unitParent != null ) && ( unitRef != null ) )
        {
            if ( unitParent.getIdUnit( ) == unitRef.getIdParent( ) )
            {
                return true;
            }

            Unit nextUnitParent = getUnit( unitRef.getIdParent( ), false );

            while ( ( nextUnitParent != null ) && ( nextUnitParent.getIdUnit( ) != Unit.ID_NULL ) )
            {
                if ( unitParent.getIdUnit( ) == nextUnitParent.getIdUnit( ) )
                {
                    return true;
                }

                nextUnitParent = getUnit( nextUnitParent.getIdParent( ), false );
            }
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean canCreateSubUnit( int nIdUnit )
    {
        return hasSubUnits( nIdUnit ) || UnitAttributeManager.canCreateSubUnit( nIdUnit );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAuthorized( Unit unit, String strPermission, AdminUser user, UnittreeRBACRecursiveType recursiveType )
    {
        if ( user.isAdmin( ) )
        {
            return true;
        }

        List<Unit> listUnits = new ArrayList<>( );

        switch( recursiveType )
        {
            case PARENT_RECURSIVE:
                listUnits = getListParentUnits( unit );
                break;
            case NOT_RECURSIVE:
                listUnits.add( unit );
                break;
            default:
                // Nothing to do
        }

        return isAuthorizedForAtLeastOneUnit( listUnits, strPermission, user );
    }

    /**
     * Check that a given user has the given permission for at least one unit of the given list.
     * 
     * @param listUnits
     *            the unit list
     * @param strPermission
     *            the permission needed
     * @param user
     *            the user trying to access the resource
     * @return {code true} if the given user has the given permission for at least one unit, {@code false} otherwise
     */
    private boolean isAuthorizedForAtLeastOneUnit( List<Unit> listUnits, String strPermission, AdminUser user )
    {
        boolean bIsAuthorized = false;

        for ( Unit unitToCheck : listUnits )
        {
            if ( RBACService.isAuthorized( Unit.RESOURCE_TYPE, String.valueOf( unitToCheck.getIdUnit( ) ), strPermission, (User) user ) )
            {
                bIsAuthorized = true;
                break;
            }
        }

        return bIsAuthorized;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isAuthorized( String strIdUnit, String strPermission, AdminUser user, UnittreeRBACRecursiveType recursiveType )
    {
        if ( StringUtils.isNotBlank( strIdUnit ) && StringUtils.isNumeric( strIdUnit ) )
        {
            int nIdUnit = Integer.parseInt( strIdUnit );
            Unit unit = getUnit( nIdUnit, false );

            return isAuthorized( unit, strPermission, user, recursiveType );
        }

        return false;
    }

    // CRUD OPERATIONS

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional( "unittree.transactionManager" )
    public int createUnit( Unit unit, HttpServletRequest request ) throws UnitErrorException
    {
        if ( unit != null )
        {
            int nIdUnit = UnitHome.create( unit );
            UnitAttributeManager.doCreateUnit( unit, request );

            return nIdUnit;
        }

        return Unit.ID_NULL;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional( "unittree.transactionManager" )
    public void removeUnit( int nIdUnit, HttpServletRequest request )
    {
        if ( ( nIdUnit != Unit.ID_ROOT ) && !hasSubUnits( nIdUnit ) )
        {
            UnitAttributeManager.doRemoveUnit( nIdUnit, request );

            // Remove users
            _unitUserService.removeUsersFromUnit( nIdUnit );

            UnitHome.remove( nIdUnit );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional( "unittree.transactionManager" )
    public void updateUnit( Unit unit, HttpServletRequest request ) throws UnitErrorException
    {
        if ( unit != null )
        {
            UnitAttributeManager.doModifyUnit( unit, request );

            // Update unit information
            UnitHome.update( unit );
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional( "unittree.transactionManager" )
    public boolean moveSubTree( Unit unitToMove, Unit newUnitParent )
    {
        if ( ( unitToMove != null ) && ( newUnitParent != null ) && ( unitToMove.getIdUnit( ) != newUnitParent.getIdUnit( ) )
                && !isParent( unitToMove, newUnitParent ) )
        {
            UnitAttributeManager.moveSubTree( unitToMove, newUnitParent );
            unitToMove.setIdParent( newUnitParent.getIdUnit( ) );
            UnitHome.updateParent( unitToMove.getIdUnit( ), newUnitParent.getIdUnit( ) );

            return true;
        }

        return false;
    }

    // PRIVATE METHODS

    /**
     * Get the XML for an unit
     * 
     * @param sbXML
     *            the XML
     * @param listAuthorizedUnits
     *            the list of authorized units
     * @param unit
     *            the unit
     * @param user
     *            the user for which the XML is built
     */
    private void getXMLUnit( StringBuffer sbXML, List<Unit> listAuthorizedUnits, Unit unit, AdminUser user )
    {
        XmlUtil.beginElement( sbXML, TAG_UNIT );
        XmlUtil.addElement( sbXML, TAG_ID_UNIT, unit.getIdUnit( ) );
        XmlUtil.addElement( sbXML, TAG_LABEL, CDATA_START + unit.getLabel( ) + CDATA_END );
        XmlUtil.addElement( sbXML, TAG_DESCRIPTION, CDATA_START + unit.getDescription( ) + CDATA_END );

        List<Unit> listChildren = getSubUnits( unit.getIdUnit( ), false );

        if ( ( listChildren != null ) && !listChildren.isEmpty( ) )
        {
            XmlUtil.beginElement( sbXML, TAG_UNIT_CHILDREN );

            for ( Unit child : listChildren )
            {
                if ( isUnitInList( child, listAuthorizedUnits ) )
                {
                    getXMLUnit( sbXML, listAuthorizedUnits, child, user );
                }
            }

            XmlUtil.endElement( sbXML, TAG_UNIT_CHILDREN );
        }

        XmlUtil.endElement( sbXML, TAG_UNIT );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getListParentUnits( Unit givenUnit )
    {
        if ( givenUnit == null )
        {
            return new ArrayList<>( );
        }

        List<Unit> listAllUnits = UnitHome.findAll( );
        List<Unit> listParentUnits = new ArrayList<>( );

        // Build a map of id / unit :
        Map<Integer, Unit> mapIdUnit = new HashMap<>( );
        for ( Unit unit : listAllUnits )
        {
            mapIdUnit.put( unit.getIdUnit( ), unit );
        }

        Unit recursiveUnit = givenUnit;
        while ( recursiveUnit != null )
        {
            listParentUnits.add( recursiveUnit );
            if ( recursiveUnit.getIdParent( ) == -1 )
            {
                break;
            }
            recursiveUnit = mapIdUnit.get( recursiveUnit.getIdParent( ) );

        }
        return listParentUnits;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isUnitInList( Unit unitToCheck, Collection<Unit> listUnits )
    {
        for ( Unit unit : listUnits )
        {
            if ( unit.getIdUnit( ) == unitToCheck.getIdUnit( ) )
            {
                return true;
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Unit getUnitByCode( String strCode, boolean bGetAdditionalInfos )
    {

        return UnitHome.findByCode( strCode );
    }
}