View Javadoc
1   /*
2    * Copyright (c) 2002-2018, Mairie de Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.stock.modules.recommendation.service;
35  
36  import fr.paris.lutece.plugins.stock.modules.recommendation.business.AvailableProductsDAO;
37  import fr.paris.lutece.plugins.stock.modules.recommendation.business.Recommendation;
38  import fr.paris.lutece.plugins.stock.modules.recommendation.business.RecommendationDAO;
39  import fr.paris.lutece.plugins.stock.modules.recommendation.business.RecommendedProduct;
40  import fr.paris.lutece.plugins.stock.modules.recommendation.business.StockPurchaseDAO;
41  import fr.paris.lutece.plugins.stock.modules.recommendation.business.UserItem;
42  import fr.paris.lutece.portal.service.plugin.Plugin;
43  import fr.paris.lutece.portal.service.plugin.PluginService;
44  import fr.paris.lutece.portal.service.util.AppLogService;
45  import fr.paris.lutece.portal.service.util.AppPathService;
46  import fr.paris.lutece.portal.service.util.AppPropertiesService;
47  import java.io.File;
48  import java.io.FileNotFoundException;
49  import java.io.IOException;
50  import java.sql.Timestamp;
51  import java.util.ArrayList;
52  import java.util.Date;
53  import java.util.List;
54  import org.apache.mahout.cf.taste.common.NoSuchUserException;
55  import org.apache.mahout.cf.taste.common.TasteException;
56  import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
57  import org.apache.mahout.cf.taste.impl.model.file.FileIDMigrator;
58  import org.apache.mahout.cf.taste.impl.neighborhood.ThresholdUserNeighborhood;
59  import org.apache.mahout.cf.taste.impl.recommender.GenericBooleanPrefUserBasedRecommender;
60  import org.apache.mahout.cf.taste.impl.similarity.LogLikelihoodSimilarity;
61  import org.apache.mahout.cf.taste.model.DataModel;
62  import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
63  import org.apache.mahout.cf.taste.recommender.RecommendedItem;
64  import org.apache.mahout.cf.taste.recommender.UserBasedRecommender;
65  import org.apache.mahout.cf.taste.similarity.UserSimilarity;
66  
67  /**
68   * StockRecommendationService
69   */
70  public final class StockRecommendationService
71  {
72      private static final String PLUGIN_NAME = "stock-recommendation";
73      private static final String PROPERTY_ID_MIGRATOR_FILE_PATH = "stock-recommendation.idMigratorFilePath";
74      private static final String PROPERTY_DATA_FILE_PATH = "stock-recommendation.dataFilePath";
75      private static final String PROPERTY_THRESHOLD = "stock-recommendation.recommender.threshold";
76      private static final String PROPERTY_COUNT = "stock-recommendation.recommender.count";
77      private static final String DEFAULT_THRESHOLD = "0.1";
78      private static final int DEFAULT_COUNT = 6;
79  
80      private static Plugin _plugin = PluginService.getPlugin( PLUGIN_NAME );
81      private static StockPurchaseDAO _daoPurchase = new StockPurchaseDAO( );
82      private static RecommendationDAO _daoRecommendation = new RecommendationDAO( );
83      private static AvailableProductsDAO _daoProducts = new AvailableProductsDAO( );
84      private static FileIDMigrator _migrator;
85      private static StockRecommendationService _singleton;
86      private static PurchaseDataWriter _writer;
87      private static UserBasedRecommender _recommender;
88      private static int _nCount = AppPropertiesService.getPropertyInt( PROPERTY_COUNT, DEFAULT_COUNT );
89      private static List<Integer> _listAvailableProducts;
90  
91      /** private constructor */
92      private StockRecommendationService( )
93      {
94      }
95  
96      /**
97       * Return the unique instance
98       * 
99       * @return The unique instance
100      */
101     public static StockRecommendationService instance( )
102     {
103         String strDataFilePath = AppPropertiesService.getProperty( PROPERTY_DATA_FILE_PATH );
104         File dataFile = new File( AppPathService.getAbsolutePathFromRelativePath( strDataFilePath ) );
105         if ( _singleton == null || dataFile.length() <= 0L )
106         {
107             synchronized( StockRecommendationService.class )
108             {
109                 _singleton = new StockRecommendationService( );
110                 String strIdMigratorFilePath = AppPropertiesService.getProperty( PROPERTY_ID_MIGRATOR_FILE_PATH );
111                 File idMigratorFile = new File( AppPathService.getAbsolutePathFromRelativePath( strIdMigratorFilePath ) );
112 
113                 try
114                 {
115                     _migrator = new FileIDMigrator( idMigratorFile );
116 
117                     _listAvailableProducts = buildAvailableProductsList( );
118                     AppLogService.info( "stock-recommendation : " + _listAvailableProducts.size( ) + " products found that can be ordered." );
119                     AppLogService.info( "stock-recommendation : creating data file with current purchases." );
120                     _writer = new FilePurchaseDataWriter( dataFile );
121                     extractPurchases( );
122                     AppLogService.info( "stock-recommendation : initialize the recommender with data." );
123                     if( dataFile.length() > 0L ) {
124                         _recommender = createRecommender(dataFile);
125                     }
126                 }
127                 catch( FileNotFoundException ex )
128                 {
129                     AppLogService.error( "stock-recommendation : Error creating file " + strIdMigratorFilePath + " " + ex.getMessage( ), ex );
130                 }
131                 catch( IOException ex )
132                 {
133                     AppLogService.error( "stock-recommendation : Error creating file " + strIdMigratorFilePath + " " + ex.getMessage( ), ex );
134                 }
135             }
136         }
137         return _singleton;
138     }
139 
140     /**
141      * Extract purchase from the database and write data using the selected DataWriter implementation
142      */
143     public static void extractPurchases( )
144     {
145         _writer.reset( );
146         List<UserItem> list = _daoPurchase.selectUserItemsList( _plugin );
147         for ( UserItem ui : list )
148         {
149             long lUserID = _migrator.toLongID( ui.getUserName( ) );
150             _writer.write( lUserID, ui.getItemId( ) );
151 
152         }
153         AppLogService.info( "stock-recommendation : retieved purchases count : " + list.size( ) );
154         _writer.close( );
155     }
156 
157     /**
158      * Get the list of recommended items for a given user
159      * 
160      * @param strUserName
161      *            The User ID
162      * @return The list
163      * @throws NoSuchUserException
164      *             if the user is not found
165      * @throws TasteException
166      *             Other problem
167      */
168     public List<RecommendedItem> getRecommendedItems( String strUserName ) throws NoSuchUserException, TasteException
169     {
170         long lUserId = _migrator.toLongID( strUserName );
171         List<RecommendedItem> recommendedItems = new ArrayList<>();
172         if(_recommender != null ) {
173             recommendedItems = _recommender.recommend( lUserId, _nCount );
174         }
175         return recommendedItems;
176     }
177 
178     /**
179      * Get the list of recommended products for a given user
180      * 
181      * @param strUserName
182      *            The User ID
183      * @return The list
184      * @throws NoSuchUserException
185      *             if the user is not found
186      * @throws TasteException
187      *             Other problem
188      */
189     public List<RecommendedProduct> getRecommendedProducts( String strUserName ) throws NoSuchUserException, TasteException
190     {
191         // Search in database
192         List<RecommendedProduct> list = loadFromDatabase( strUserName );
193 
194         if ( list != null )
195         {
196             return list;
197         }
198 
199         // build the list
200         list = getRecommendedProductsList( strUserName );
201 
202         // save data in database
203         writeUserRecommendations( strUserName, list );
204 
205         return list;
206 
207     }
208 
209     /**
210      * Get the recommended products list
211      * 
212      * @param strUserName
213      *            The username
214      * @return The list
215      * @throws NoSuchUserException
216      *             if the user is not found
217      * @throws TasteException
218      *             if an error occurs
219      */
220     private List<RecommendedProduct> getRecommendedProductsList( String strUserName ) throws NoSuchUserException, TasteException
221     {
222         List<RecommendedProduct> list = new ArrayList<>( );
223 
224         for ( RecommendedItem item : getRecommendedItems( strUserName ) )
225         {
226             int nItemId = (int) item.getItemID( );
227             if ( _listAvailableProducts.contains( nItemId ) )
228             {
229                 RecommendedProduct product = new RecommendedProduct( );
230                 product.setProductId( nItemId );
231                 product.setScore( item.getValue( ) );
232                 _daoProducts.getProductInfos( product, _plugin );
233                 list.add( product );
234                 AppLogService.info( "Product recommended for user " + strUserName + " : " + product );
235             }
236         }
237         return list;
238     }
239 
240     /**
241      * Load recommended products from the database for a given user
242      * 
243      * @param strUsername
244      *            The user
245      * @return The list
246      */
247     private List<RecommendedProduct> loadFromDatabase( String strUsername )
248     {
249         List<Recommendation> listRecommandations = _daoRecommendation.selectRecommendationsByUser( strUsername, _plugin );
250 
251         if ( listRecommandations == null || listRecommandations.isEmpty( ) )
252         {
253             return null;
254         }
255 
256         List<RecommendedProduct> list = new ArrayList<>( );
257         for ( Recommendation recommendation : listRecommandations )
258         {
259             RecommendedProduct product = new RecommendedProduct( );
260             product.setProductId( recommendation.getIdProduct( ) );
261             product.setScore( recommendation.getScore( ) );
262             _daoProducts.getProductInfos( product, _plugin );
263             list.add( product );
264         }
265         return list;
266 
267     }
268 
269     /**
270      * Create a recommender
271      * 
272      * @param fileData
273      *            The data file
274      * @return The recommender
275      * @throws IOException
276      *             if a problem occurs with the data file
277      */
278     private static UserBasedRecommender createRecommender( File fileData ) throws IOException
279     {
280         DataModel model = new FileDataModel( fileData );
281         UserSimilarity similarity = new LogLikelihoodSimilarity( model );
282 
283         String strThreshold = AppPropertiesService.getProperty( PROPERTY_THRESHOLD, DEFAULT_THRESHOLD );
284         double threshold = Double.valueOf( strThreshold );
285         UserNeighborhood neighborhood = new ThresholdUserNeighborhood( threshold, similarity, model );
286         return new GenericBooleanPrefUserBasedRecommender( model, neighborhood, similarity );
287 
288     }
289 
290     /**
291      * Build the available productlist
292      * 
293      * @return The list of IDs
294      */
295     private static List<Integer> buildAvailableProductsList( )
296     {
297         Timestamp time = new Timestamp( ( new Date( ) ).getTime( ) );
298         List<Integer> listAvailableProducts = _daoProducts.selectAvailableProductsIdList( time, _plugin );
299         return listAvailableProducts;
300     }
301 
302     /**
303      * build recommendations (used by the daemon)
304      * 
305      * @param sbLogs
306      *            The daemons logs
307      */
308     public void buildRecommendations( StringBuilder sbLogs )
309     {
310         List<String> listUsers = _daoPurchase.selectUsersList( _plugin );
311         int nUsersCount = listUsers.size( );
312         int nRecommendationCount = 0;
313         AppLogService.info( "Starting building recommendations for " + nUsersCount + " users ..." );
314         for ( String strUsername : listUsers )
315         {
316             List<RecommendedProduct> listProduct;
317             try
318             {
319                 listProduct = getRecommendedProductsList( strUsername );
320                 nRecommendationCount += writeUserRecommendations( strUsername, listProduct );
321             }
322             catch( TasteException ex )
323             {
324                 AppLogService.error( "stock-recommendation : Error building recommendations : " + ex.getMessage( ), ex );
325             }
326         }
327 
328         double ratio = (double) nRecommendationCount / (double) nUsersCount;
329         sbLogs.append( "Recommendation builder\n " ).append( "number of users : " ).append( nUsersCount ).append( '\n' )
330                 .append( "number of recommendations : " ).append( nRecommendationCount ).append( '\n' ).append( "ratio per user : " ).append( ratio )
331                 .append( '\n' );
332     }
333 
334     /**
335      * Write recommendations into the database for a given user
336      * 
337      * @param strUsername
338      *            The Username
339      * @param listProduct
340      *            The product list
341      * @return The number of recommendations found
342      */
343     private int writeUserRecommendations( String strUsername, List<RecommendedProduct> listProduct )
344     {
345         int nCount = 0;
346         _daoRecommendation.deleteByUser( strUsername, _plugin );
347         for ( RecommendedProduct product : listProduct )
348         {
349             Recommendation recommendation = new Recommendation( );
350             recommendation.setUsername( strUsername );
351             recommendation.setIdProduct( product.getProductId( ) );
352             recommendation.setScore( product.getScore( ) );
353             _daoRecommendation.insert( recommendation, _plugin );
354             nCount++;
355         }
356 
357         return nCount;
358 
359     }
360 }