StockRecommendationService.java

/*
 * Copyright (c) 2002-2018, Mairie de 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.stock.modules.recommendation.service;

import fr.paris.lutece.plugins.stock.modules.recommendation.business.AvailableProductsDAO;
import fr.paris.lutece.plugins.stock.modules.recommendation.business.Recommendation;
import fr.paris.lutece.plugins.stock.modules.recommendation.business.RecommendationDAO;
import fr.paris.lutece.plugins.stock.modules.recommendation.business.RecommendedProduct;
import fr.paris.lutece.plugins.stock.modules.recommendation.business.StockPurchaseDAO;
import fr.paris.lutece.plugins.stock.modules.recommendation.business.UserItem;
import fr.paris.lutece.portal.service.plugin.Plugin;
import fr.paris.lutece.portal.service.plugin.PluginService;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.service.util.AppPathService;
import fr.paris.lutece.portal.service.util.AppPropertiesService;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.mahout.cf.taste.common.NoSuchUserException;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
import org.apache.mahout.cf.taste.impl.model.file.FileIDMigrator;
import org.apache.mahout.cf.taste.impl.neighborhood.ThresholdUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericBooleanPrefUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.LogLikelihoodSimilarity;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.cf.taste.recommender.UserBasedRecommender;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;

/**
 * StockRecommendationService
 */
public final class StockRecommendationService
{
    private static final String PLUGIN_NAME = "stock-recommendation";
    private static final String PROPERTY_ID_MIGRATOR_FILE_PATH = "stock-recommendation.idMigratorFilePath";
    private static final String PROPERTY_DATA_FILE_PATH = "stock-recommendation.dataFilePath";
    private static final String PROPERTY_THRESHOLD = "stock-recommendation.recommender.threshold";
    private static final String PROPERTY_COUNT = "stock-recommendation.recommender.count";
    private static final String DEFAULT_THRESHOLD = "0.1";
    private static final int DEFAULT_COUNT = 6;

    private static Plugin _plugin = PluginService.getPlugin( PLUGIN_NAME );
    private static StockPurchaseDAO _daoPurchase = new StockPurchaseDAO( );
    private static RecommendationDAO _daoRecommendation = new RecommendationDAO( );
    private static AvailableProductsDAO _daoProducts = new AvailableProductsDAO( );
    private static FileIDMigrator _migrator;
    private static StockRecommendationService _singleton;
    private static PurchaseDataWriter _writer;
    private static UserBasedRecommender _recommender;
    private static int _nCount = AppPropertiesService.getPropertyInt( PROPERTY_COUNT, DEFAULT_COUNT );
    private static List<Integer> _listAvailableProducts;

    /** private constructor */
    private StockRecommendationService( )
    {
    }

    /**
     * Return the unique instance
     * 
     * @return The unique instance
     */
    public static StockRecommendationService instance( )
    {
        String strDataFilePath = AppPropertiesService.getProperty( PROPERTY_DATA_FILE_PATH );
        File dataFile = new File( AppPathService.getAbsolutePathFromRelativePath( strDataFilePath ) );
        if ( _singleton == null || dataFile.length() <= 0L )
        {
            synchronized( StockRecommendationService.class )
            {
                _singleton = new StockRecommendationService( );
                String strIdMigratorFilePath = AppPropertiesService.getProperty( PROPERTY_ID_MIGRATOR_FILE_PATH );
                File idMigratorFile = new File( AppPathService.getAbsolutePathFromRelativePath( strIdMigratorFilePath ) );

                try
                {
                    _migrator = new FileIDMigrator( idMigratorFile );

                    _listAvailableProducts = buildAvailableProductsList( );
                    AppLogService.info( "stock-recommendation : " + _listAvailableProducts.size( ) + " products found that can be ordered." );
                    AppLogService.info( "stock-recommendation : creating data file with current purchases." );
                    _writer = new FilePurchaseDataWriter( dataFile );
                    extractPurchases( );
                    AppLogService.info( "stock-recommendation : initialize the recommender with data." );
                    if( dataFile.length() > 0L ) {
                        _recommender = createRecommender(dataFile);
                    }
                }
                catch( FileNotFoundException ex )
                {
                    AppLogService.error( "stock-recommendation : Error creating file " + strIdMigratorFilePath + " " + ex.getMessage( ), ex );
                }
                catch( IOException ex )
                {
                    AppLogService.error( "stock-recommendation : Error creating file " + strIdMigratorFilePath + " " + ex.getMessage( ), ex );
                }
            }
        }
        return _singleton;
    }

    /**
     * Extract purchase from the database and write data using the selected DataWriter implementation
     */
    public static void extractPurchases( )
    {
        _writer.reset( );
        List<UserItem> list = _daoPurchase.selectUserItemsList( _plugin );
        for ( UserItem ui : list )
        {
            long lUserID = _migrator.toLongID( ui.getUserName( ) );
            _writer.write( lUserID, ui.getItemId( ) );

        }
        AppLogService.info( "stock-recommendation : retieved purchases count : " + list.size( ) );
        _writer.close( );
    }

    /**
     * Get the list of recommended items for a given user
     * 
     * @param strUserName
     *            The User ID
     * @return The list
     * @throws NoSuchUserException
     *             if the user is not found
     * @throws TasteException
     *             Other problem
     */
    public List<RecommendedItem> getRecommendedItems( String strUserName ) throws NoSuchUserException, TasteException
    {
        long lUserId = _migrator.toLongID( strUserName );
        List<RecommendedItem> recommendedItems = new ArrayList<>();
        if(_recommender != null ) {
            recommendedItems = _recommender.recommend( lUserId, _nCount );
        }
        return recommendedItems;
    }

    /**
     * Get the list of recommended products for a given user
     * 
     * @param strUserName
     *            The User ID
     * @return The list
     * @throws NoSuchUserException
     *             if the user is not found
     * @throws TasteException
     *             Other problem
     */
    public List<RecommendedProduct> getRecommendedProducts( String strUserName ) throws NoSuchUserException, TasteException
    {
        // Search in database
        List<RecommendedProduct> list = loadFromDatabase( strUserName );

        if ( list != null )
        {
            return list;
        }

        // build the list
        list = getRecommendedProductsList( strUserName );

        // save data in database
        writeUserRecommendations( strUserName, list );

        return list;

    }

    /**
     * Get the recommended products list
     * 
     * @param strUserName
     *            The username
     * @return The list
     * @throws NoSuchUserException
     *             if the user is not found
     * @throws TasteException
     *             if an error occurs
     */
    private List<RecommendedProduct> getRecommendedProductsList( String strUserName ) throws NoSuchUserException, TasteException
    {
        List<RecommendedProduct> list = new ArrayList<>( );

        for ( RecommendedItem item : getRecommendedItems( strUserName ) )
        {
            int nItemId = (int) item.getItemID( );
            if ( _listAvailableProducts.contains( nItemId ) )
            {
                RecommendedProduct product = new RecommendedProduct( );
                product.setProductId( nItemId );
                product.setScore( item.getValue( ) );
                _daoProducts.getProductInfos( product, _plugin );
                list.add( product );
                AppLogService.info( "Product recommended for user " + strUserName + " : " + product );
            }
        }
        return list;
    }

    /**
     * Load recommended products from the database for a given user
     * 
     * @param strUsername
     *            The user
     * @return The list
     */
    private List<RecommendedProduct> loadFromDatabase( String strUsername )
    {
        List<Recommendation> listRecommandations = _daoRecommendation.selectRecommendationsByUser( strUsername, _plugin );

        if ( listRecommandations == null || listRecommandations.isEmpty( ) )
        {
            return null;
        }

        List<RecommendedProduct> list = new ArrayList<>( );
        for ( Recommendation recommendation : listRecommandations )
        {
            RecommendedProduct product = new RecommendedProduct( );
            product.setProductId( recommendation.getIdProduct( ) );
            product.setScore( recommendation.getScore( ) );
            _daoProducts.getProductInfos( product, _plugin );
            list.add( product );
        }
        return list;

    }

    /**
     * Create a recommender
     * 
     * @param fileData
     *            The data file
     * @return The recommender
     * @throws IOException
     *             if a problem occurs with the data file
     */
    private static UserBasedRecommender createRecommender( File fileData ) throws IOException
    {
        DataModel model = new FileDataModel( fileData );
        UserSimilarity similarity = new LogLikelihoodSimilarity( model );

        String strThreshold = AppPropertiesService.getProperty( PROPERTY_THRESHOLD, DEFAULT_THRESHOLD );
        double threshold = Double.valueOf( strThreshold );
        UserNeighborhood neighborhood = new ThresholdUserNeighborhood( threshold, similarity, model );
        return new GenericBooleanPrefUserBasedRecommender( model, neighborhood, similarity );

    }

    /**
     * Build the available productlist
     * 
     * @return The list of IDs
     */
    private static List<Integer> buildAvailableProductsList( )
    {
        Timestamp time = new Timestamp( ( new Date( ) ).getTime( ) );
        List<Integer> listAvailableProducts = _daoProducts.selectAvailableProductsIdList( time, _plugin );
        return listAvailableProducts;
    }

    /**
     * build recommendations (used by the daemon)
     * 
     * @param sbLogs
     *            The daemons logs
     */
    public void buildRecommendations( StringBuilder sbLogs )
    {
        List<String> listUsers = _daoPurchase.selectUsersList( _plugin );
        int nUsersCount = listUsers.size( );
        int nRecommendationCount = 0;
        AppLogService.info( "Starting building recommendations for " + nUsersCount + " users ..." );
        for ( String strUsername : listUsers )
        {
            List<RecommendedProduct> listProduct;
            try
            {
                listProduct = getRecommendedProductsList( strUsername );
                nRecommendationCount += writeUserRecommendations( strUsername, listProduct );
            }
            catch( TasteException ex )
            {
                AppLogService.error( "stock-recommendation : Error building recommendations : " + ex.getMessage( ), ex );
            }
        }

        double ratio = (double) nRecommendationCount / (double) nUsersCount;
        sbLogs.append( "Recommendation builder\n " ).append( "number of users : " ).append( nUsersCount ).append( '\n' )
                .append( "number of recommendations : " ).append( nRecommendationCount ).append( '\n' ).append( "ratio per user : " ).append( ratio )
                .append( '\n' );
    }

    /**
     * Write recommendations into the database for a given user
     * 
     * @param strUsername
     *            The Username
     * @param listProduct
     *            The product list
     * @return The number of recommendations found
     */
    private int writeUserRecommendations( String strUsername, List<RecommendedProduct> listProduct )
    {
        int nCount = 0;
        _daoRecommendation.deleteByUser( strUsername, _plugin );
        for ( RecommendedProduct product : listProduct )
        {
            Recommendation recommendation = new Recommendation( );
            recommendation.setUsername( strUsername );
            recommendation.setIdProduct( product.getProductId( ) );
            recommendation.setScore( product.getScore( ) );
            _daoRecommendation.insert( recommendation, _plugin );
            nCount++;
        }

        return nCount;

    }
}