SolrProposalIndexer.java

/*
 * Copyright (c) 2002-2020, 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.participatoryideation.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.apache.commons.lang.StringUtils;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.common.SolrInputDocument;

import fr.paris.lutece.plugins.extend.modules.rating.business.Rating;
import fr.paris.lutece.plugins.extend.modules.rating.service.IRatingService;
import fr.paris.lutece.plugins.participatoryideation.business.proposal.Proposal;
import fr.paris.lutece.plugins.participatoryideation.business.proposal.ProposalHome;
import fr.paris.lutece.plugins.participatoryideation.business.proposal.ProposalSearcher;
import fr.paris.lutece.plugins.participatoryideation.util.ParticipatoryIdeationConstants;
import fr.paris.lutece.plugins.participatoryideation.web.IdeationApp;
import fr.paris.lutece.plugins.search.solr.business.SolrServerService;
import fr.paris.lutece.plugins.search.solr.business.field.Field;
import fr.paris.lutece.plugins.search.solr.indexer.SolrIndexer;
import fr.paris.lutece.plugins.search.solr.indexer.SolrIndexerService;
import fr.paris.lutece.plugins.search.solr.indexer.SolrItem;
import fr.paris.lutece.plugins.search.solr.util.SolrConstants;
import fr.paris.lutece.plugins.workflowcore.business.state.State;
import fr.paris.lutece.portal.service.prefs.UserPreferencesService;
import fr.paris.lutece.portal.service.search.SearchItem;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import fr.paris.lutece.portal.service.workflow.WorkflowService;
import fr.paris.lutece.util.url.UrlItem;

/**
 * Solr Indexer of Proposals
 * @author emilio
 *
 */
public class SolrProposalIndexer implements SolrIndexer
{

    private static final String SHORT_NAME = "proposal";
    private static final String PARAMETER_XPAGE = "page";
    private static final String XPAGE_PROPOSAL = "proposal";
    private static final String PARAMETER_CODE_CAMPAIGN = "campaign";
    private static final String PARAMETER_CODE_PROPOSAL = "proposal";

    @Inject
    private IRatingService _ratingService;
    
    /**
     * Used in one by one indexing, access directly to the Apache Solr client
     * @param proposal
     */
    public void writeProposal( Proposal proposal )
    {
        try
        {
            write( getItem( proposal ) );
        }
        catch( Exception e )
        {
            AppLogService.error( "[SolrProposalIndexer] Error during write (proposal " + proposal.getReference( ) + ") " + e.getMessage( ), e );
        }
    }
    
    /**
     * Used in one by one indexing, access directly to the Apache Solr client
     * @param proposal
     */
    public void removeProposal( Proposal proposal )
    {

        try
        {
            SolrClient SOLR_SERVER = SolrServerService.getInstance( ).getSolrServer( );
            SOLR_SERVER.deleteByQuery(
                    SearchItem.FIELD_UID + ":" + SolrIndexerService.getWebAppName( ) + "_" + getResourceUid( Integer.toString( proposal.getId( ) ), null ) );
            SOLR_SERVER.commit( );

        }
        catch( Exception e )
        {
            AppLogService.error( "[SolrProposalIndexer] Error during remove (proposal " + proposal.getReference( ) + ") " + e.getMessage( ), e );
        }
    }
    
    /**
     * 
     * @param proposal
     * @return
     * @throws IOException
     */
    public SolrItem getItem( Proposal proposal ) throws IOException
    {
        // the item
        SolrItem item = new SolrItem( );
        item.setUid( getResourceUid( Integer.toString( proposal.getId( ) ), null ) );
        item.setDate( proposal.getCreationTimestamp( ) );
        item.setType( "proposal" );
        item.setSummary( proposal.getDescription( ) );
        item.setTitle( proposal.getTitre( ) );
        item.setSite( SolrIndexerService.getWebAppName( ) );
        item.setRole( "none" );

        String strCodeGeoloc;
        double dLongitude = 0;
        double dLatitude = 0;

        if ( proposal.getAdress( ) != null && proposal.getLongitude( ) != null && proposal.getLatitude( ) != null )
        {
            dLongitude = proposal.getLongitude( );
            dLatitude = proposal.getLatitude( );
            if ( Proposal.LOCATION_AREA_TYPE_LOCALIZED.equals( proposal.getLocationType( ) ) )
            {
                strCodeGeoloc = "proposal_geoloc-ardt-" + proposal.getLocationArdt( );
            }
            else
            {
                strCodeGeoloc = "proposal_geoloc-paris";
            }
        }
        else
        {
            if ( Proposal.LOCATION_AREA_TYPE_LOCALIZED.equals( proposal.getLocationType( ) ) )
            {
                strCodeGeoloc = "proposal_ardt-" + proposal.getLocationArdt( );
            }
            else
            {
                strCodeGeoloc = "proposal_paris";
            }
        }
        item.addDynamicFieldGeoloc( "proposal", proposal.getAdress( ), dLongitude, dLatitude, strCodeGeoloc );
        item.addDynamicField( "proposal_status", String.valueOf( proposal.getStatusPublic( ).isPublished( ) ) );
        item.addDynamicFieldNotAnalysed( "status", String.valueOf( proposal.getStatusPublic( ).getValeur( ) ) );
        item.addDynamicFieldNotAnalysed( "code_theme", proposal.getCodeTheme( ) );
        item.addDynamicFieldNotAnalysed( "code_submitter_type", proposal.getSubmitterType( ) );
        item.addDynamicField( "campaign", proposal.getCodeCampaign( ) );
        item.addDynamicField( "code_projet", (long) proposal.getCodeProposal( ) );
        item.addDynamicField( "location", ( ( proposal.getAdress( ) != null ) && ( !"".equals( proposal.getAdress( ).trim( ) ) ) ) ? proposal.getAdress( )
                : ( Proposal.LOCATION_AREA_TYPE_LOCALIZED.equals( proposal.getLocationType( ).trim( ) ) ? proposal.getLocationArdt( ) : "whole city" // TODO :
                // Must
                // get
                // this string from
                // campaign area
                // service
                ) );
        item.addDynamicFieldNotAnalysed( "location_type", proposal.getLocationType( ) );

        int idWorkflow = AppPropertiesService.getPropertyInt( ParticipatoryIdeationConstants.PROPERTY_WORKFLOW_ID, -1 );
        State state = WorkflowService.getInstance( ).getState( proposal.getId( ), Proposal.WORKFLOW_RESOURCE_TYPE, idWorkflow, -1 );
        item.addDynamicField( "workflow_id_state", (long) state.getId() );

        if ( Proposal.LOCATION_AREA_TYPE_LOCALIZED.equals( proposal.getLocationType( ).trim( ) ) )
        {
            item.addDynamicField( "location_ardt", proposal.getLocationArdt( ) );
        }

        item.addDynamicField( "budget", proposal.getCout( ) );

        item.addDynamicFieldNotAnalysed( "type_qpvqva", proposal.getTypeQpvQva( ) );
        if ( IdeationApp.QPV_QVA_QPV.equals( proposal.getTypeQpvQva( ) ) || IdeationApp.QPV_QVA_QVA.equals( proposal.getTypeQpvQva( ) )
                || IdeationApp.QPV_QVA_GPRU.equals( proposal.getTypeQpvQva( ) ) || IdeationApp.QPV_QVA_QBP.equals( proposal.getTypeQpvQva( ) ) )
        {
            item.addDynamicField( "libelle_qpvqva", proposal.getLibelleQpvQva( ) );
        }

        item.addDynamicFieldNotAnalysed( "url_projet", proposal.getUrlProjet( ) );
        item.addDynamicFieldNotAnalysed( "winner_projet", proposal.getWinnerProjet( ) );

        if ( proposal.getStatusPublic( ).getValeur( ) != null )
        {
            item.addDynamicField( "statut_publique_project", proposal.getStatusPublic( ).getValeur( ) );
        }

        Rating rating = _ratingService.findByResource( String.valueOf( proposal.getId( ) ), Proposal.PROPERTY_RESOURCE_TYPE );

        if ( rating != null )
        {
            item.addDynamicField( "like", (long) rating.getScorePositifsVotes( ) );
            item.addDynamicField( "dislike", (long) rating.getScoreNegativesVotes( ) );
        }
        else
        {
            item.addDynamicField( "like", 0L );
            item.addDynamicField( "dislike", 0L );
        }

        item.setXmlContent( "" );
        UrlItem url = new UrlItem( SolrIndexerService.getBaseUrl( ) );
        url.addParameter( PARAMETER_XPAGE, XPAGE_PROPOSAL );
        url.addParameter( PARAMETER_CODE_CAMPAIGN, proposal.getCodeCampaign( ) );
        url.addParameter( PARAMETER_CODE_PROPOSAL, proposal.getCodeProposal( ) );

        // Date Hierarchy
        GregorianCalendar calendar = new GregorianCalendar( );
        calendar.setTime( proposal.getCreationTimestamp( ) );
        item.setHieDate( calendar.get( GregorianCalendar.YEAR ) + "/" + ( calendar.get( GregorianCalendar.MONTH ) + 1 ) + "/"
                + calendar.get( GregorianCalendar.DAY_OF_MONTH ) + "/" );

        List<String> listCategorie = new ArrayList<String>( );
        item.setCategorie( listCategorie );
        item.setUrl( url.getUrl( ) );
        StringBuilder sb = new StringBuilder( );
        sb.append( proposal.getDescription( ) + " " + proposal.getTitre( ) );
        if ( proposal.getAdress( ) != null )
        {
            sb.append( " " + proposal.getAdress( ) );
        }

        String strNickname = UserPreferencesService.instance( ).getNickname( proposal.getLuteceUserName( ) );
        if ( !StringUtils.isEmpty( strNickname ) )
        {
            sb.append( " " + strNickname );
            item.addDynamicFieldNotAnalysed( "pseudo", strNickname );
        }

        sb.append( " " + proposal.getReference( ) );

        item.setContent( sb.toString( ) );

        return item;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getResourceUid( String strResourceId, String strResourceType )
    {
        StringBuilder sb = new StringBuilder( strResourceId );
        sb.append( '_' ).append( SHORT_NAME );

        return sb.toString( );
    }

    @Override
    public List<Field> getAdditionalFields( )
    {
        // TODO Auto-generated method stub
        return new ArrayList( );
    }
    
    /**
     * Indexer Description for Solr Plugin Back Office
     * @return description 
     */
    @Override
    public String getDescription( )
    {
        return "Solr proposal indexer";
    }

    @Override
    public List<SolrItem> getDocuments( String arg0 )
    {
        // TODO Auto-generated method stub
        return new ArrayList( );
    }
    
    /**
     * Indexer Name for Solr Plugin Back Office 
     * @return name
     */
    @Override
    public String getName( )
    {
        return "SolrProposalIndexer";
    }

    @Override
    public List<String> getResourcesName( )
    {
        // TODO Auto-generated method stub
        return new ArrayList( );
    }

    /**
     * Indexer Version
     * @return version
     */
    @Override
    public String getVersion( )
    {
        return "1.0.0";
    }

    /**
     * Index all ideas for Back Office purpose.
     * Messages are logged to Solr Indexing Page in BO
     * @return 
     */
    public List<String> indexDocuments( )
    {

        // Errors and logs management
        List<String> errors = new ArrayList<String>( );
        StringBuffer sbLogs = SolrIndexerService.getSbLogs( );

        // Getting solrItems to index
        ProposalSearcher _proposalSearcher = new ProposalSearcher( );
        _proposalSearcher.setIsPublished( true );

        Collection<SolrItem> proposalsSolrItems = new ArrayList<SolrItem>( );

        Collection<Proposal> proposalsList = new ArrayList<Proposal>( );
        try
        {
            proposalsList = ProposalHome.getProposalsListSearch( _proposalSearcher );
        }
        catch( Exception e )
        {
            printIndexMessage( e, sbLogs );
            errors.add( SolrIndexerService.buildErrorMessage( e ) );
            errors.add( sbLogs.toString( ) );
        }

        for ( Proposal proposal : proposalsList )
        {
            try
            {
                proposalsSolrItems.add( getItem( proposal ) );
            }
            catch( Exception e )
            {
                printIndexMessage( e, sbLogs );
                errors.add( SolrIndexerService.buildErrorMessage( e ) );
                errors.add( sbLogs.toString( ) );
            }
        }

        try
        {
            sbLogs.append( "\nIndexing " + proposalsSolrItems.size( ) + " idea solr items, from " + proposalsList.size( ) + " ideas\n" );
            SolrIndexerService.write( proposalsSolrItems, sbLogs );
        }
        catch( Exception e )
        {
            printIndexMessage( e, sbLogs );
            errors.add( SolrIndexerService.buildErrorMessage( e ) );
            errors.add( sbLogs.toString( ) );
        }

        return errors;
    }

    @Override
    public boolean isEnable( )
    {
        // TODO Auto-generated method stub
        return true;
    }

    /**
     * Copy paste from SolrIndexer because it's a private method
     */
    private static SolrInputDocument solrItem2SolrInputDocument( SolrItem solrItem )
    {
        SolrInputDocument solrInputDocument = new SolrInputDocument( );
        String strWebappName = SolrIndexerService.getWebAppName( );

        // Prefix the uid by the name of the site. Without that, it is necessary imposible to index two resources of two different sites with the same
        // identifier
        solrInputDocument.addField( SearchItem.FIELD_UID, strWebappName + SolrConstants.CONSTANT_UNDERSCORE + solrItem.getUid( ) );
        solrInputDocument.addField( SearchItem.FIELD_DATE, solrItem.getDate( ) );
        solrInputDocument.addField( SearchItem.FIELD_TYPE, solrItem.getType( ) );
        solrInputDocument.addField( SearchItem.FIELD_SUMMARY, solrItem.getSummary( ) );
        solrInputDocument.addField( SearchItem.FIELD_TITLE, solrItem.getTitle( ) );
        solrInputDocument.addField( SolrItem.FIELD_SITE, solrItem.getSite( ) );
        solrInputDocument.addField( SearchItem.FIELD_ROLE, solrItem.getRole( ) );
        solrInputDocument.addField( SolrItem.FIELD_XML_CONTENT, solrItem.getXmlContent( ) );
        solrInputDocument.addField( SearchItem.FIELD_URL, solrItem.getUrl( ) );
        solrInputDocument.addField( SolrItem.FIELD_HIERATCHY_DATE, solrItem.getHieDate( ) );
        solrInputDocument.addField( SolrItem.FIELD_CATEGORIE, solrItem.getCategorie( ) );
        solrInputDocument.addField( SolrItem.FIELD_CONTENT, solrItem.getContent( ) );
        solrInputDocument.addField( SearchItem.FIELD_DOCUMENT_PORTLET_ID, solrItem.getDocPortletId( ) );

        // Add the dynamic fields
        // They must be declared into the schema.xml of the solr server
        Map<String, Object> mapDynamicFields = solrItem.getDynamicFields( );

        for ( String strDynamicField : mapDynamicFields.keySet( ) )
        {
            solrInputDocument.addField( strDynamicField, mapDynamicFields.get( strDynamicField ) );
        }

        return solrInputDocument;
    }

    private static void write( SolrItem solrItem )
    {
        SolrClient SOLR_SERVER = SolrServerService.getInstance( ).getSolrServer( );
        try
        {
            SolrInputDocument solrInputDocument = solrItem2SolrInputDocument( solrItem );
            SOLR_SERVER.add( solrInputDocument );
            SOLR_SERVER.commit( );
        }
        catch( Exception e )
        {
            AppLogService.error( "IdeationApp, error during indexation" + e.getMessage( ), e );
        }
    }

    /**
     * Adds the exception into the buffer and the StringBuffer
     * 
     * @param exception
     *            Exception to report
     * @param sbLogs
     *            StringBuffer to write to
     */
    private static void printIndexMessage( Exception exception, StringBuffer sbLogs )
    {
        sbLogs.append( " - ERROR : " );

        if ( exception != null )
        {
            sbLogs.append( "(" + exception.getClass( ).getName( ) + ") " + exception.getMessage( ) );
            if ( exception.getCause( ) != null )
            {
                sbLogs.append( " : " );
                sbLogs.append( exception.getCause( ).getMessage( ) );
            }
            AppLogService.error( exception.getMessage( ), exception );
        }
        else
        {
            sbLogs.append( "'exception' param is null !" );
        }

    }

}