View Javadoc

1   package org.musicontroller.core;
2   
3   import java.util.Date;
4   import java.util.HashMap;
5   import java.util.Map;
6   import java.util.Set;
7   
8   import org.varienaja.util.DateTools;
9   import org.varienaja.util.LRUMap;
10  
11  /**
12   * <p>A class for quick eventcount lookup. Songs can have any number of Events.
13   * Counting Events, which happens a lot in MusiController, is relatively slow.
14   * Events are lazily loaded by Hibernate, thus a call to getEvents() causes an
15   * additional DB-access. Caching Events would be a solution, but is not very
16   * realistic, because of the sheer amount of them. Since we're mostly interested
17   * in counts, this class helps us.</p> 
18   * 
19   * <p>The EventCountLookup takes up only 8 integers and a Date of memory per
20   * Song and User. Therefore, caching makes sense for this object. This class
21   * contains a LRU-list, to which all lookup-tables are added.</p> 
22   * @author Varienaja
23   */
24  public class EventCountLookup {
25  	protected final static int CACHESIZE = 10000;
26  	private static Map<Long, EventCountLookup> CACHE = new LRUMap<Long, EventCountLookup>("EventCountLookup",CACHESIZE);
27  	private Map<Long,InternalLookup> _perUserLookupTable;
28  	private Date _expiryDate;
29  
30  	/**
31  	 * A per user Eventcount-lookuptable.
32  	 */
33  	class InternalLookup {
34  		int _playCount;
35  		int _skipCount;
36  		int _downloadCount;
37  		int _requestCount;
38  		int _playCountLastYear;
39  		int _skipCountLastYear;
40  		int _downloadCountLastYear;
41  		int _requestCountLastYear;
42  		Date _lastPlay;
43  
44  		/**
45  		 * Creates a new Lookup-object from a Set of Events.
46  		 * @param events The Events.
47  		 */
48  		public InternalLookup(Set<Event> events, Long userid) {
49  			for (Event event : events) {
50  				if (event.getUser()!=null && userid.equals(event.getUser().getId())) {
51  					addEvent(event);
52  				}
53  			}
54  		}
55  
56  		/**
57  		 * Creates a new Lookup-object from a single Event.
58  		 * @param event The Event.
59  		 */
60  		public InternalLookup(Event event) {
61  			addEvent(event);
62  		}
63  
64  		/**
65  		 * Adds an Event to the lookup-table. All counters are updated.
66  		 * @param event The Event to be added.
67  		 */
68  		public void addEvent(Event event) {
69  			Date lastYear = DateTools.lastYear();
70  			switch (event.getEventkind()) {
71  				case Event.played : 
72  					_playCount++;
73  					if (_lastPlay==null || event.getMoment().after(_lastPlay)) {
74  						_lastPlay = event.getMoment();
75  					}
76  					break;
77  				case Event.downloaded : _downloadCount++; break;
78  				case Event.requested : _requestCount++; break;
79  				case Event.skipped : _skipCount++; break;
80  				default : ; //Do nothing.
81  			}
82  			
83  			if (event.getMoment()!=null && event.getMoment().after(lastYear)) {
84  				switch (event.getEventkind()) {
85  					case Event.played : _playCountLastYear++; break;
86  					case Event.downloaded : _downloadCountLastYear++; break;
87  					case Event.requested : _requestCountLastYear++; break;
88  					case Event.skipped : _skipCountLastYear++; break;
89  				default : ; //Do nothing.
90  				}
91  			}
92  		}
93  	}
94  	
95  	/**
96  	 * Creates a new Lookup-object from a Set of Events.
97  	 * @param events The Events.
98  	 */
99  	private EventCountLookup(Set<Event> events, long songid) {
100 		Date lastYear = DateTools.lastYear();
101 		_perUserLookupTable = new HashMap<Long,InternalLookup>();
102 		for (Event event : events) {
103 			if (event.getUser()!=null) {
104 				long userid = event.getUser().getId();
105 				if (_perUserLookupTable.get(userid)==null) {
106 					_perUserLookupTable.put(userid, new InternalLookup(events, userid));
107 				}
108 				
109 				if (event.getMoment().after(lastYear)) {
110 					//Set the expirydate. If we find the first Event after 'lastYear',
111 					//the expirydate is set to 'now'+Moment-lastYear.
112 					if (_expiryDate == null) {
113 						long now = DateTools.currentDate().getTime();
114 						long moment = event.getMoment().getTime();
115 						long lastYr = lastYear.getTime();
116 						_expiryDate = new Date(now+moment-lastYr);
117 					}
118 				}
119 			}
120 		}
121 	}
122 	
123 	/**
124 	 * Factory method, which returns an EventCountLookup object. When a cached
125 	 * object is found, it is returned, otherwise a new object is created and
126 	 * added to the cache.
127 	 * @param events The Set of Events, for the Song. (Only accessed when a new
128 	 *               EventCountLookup-object must be created. When a cached
129 	 *               object is found, the Events are not touched, so that they
130 	 *               can stay uninitialized when they were lazily loaded.)
131 	 * @param songid The id of the Song.
132 	 * @return An initialized EventCountLookup-object.
133 	 */
134 	public static EventCountLookup create(Set<Event> events, long songid) {
135 		EventCountLookup cached = CACHE.get(songid);
136 		// If we do not have a cached element OR we have a cached element, but
137 		// it has expired, we build a new lookup-table.
138 		if (cached==null || (cached._expiryDate!=null && cached._expiryDate.before(new Date()))) {
139 			EventCountLookup result = new EventCountLookup(events, songid);
140 			CACHE.put(songid, result);
141 			return result;
142 		} else {
143 			return cached;
144 		}
145 	}
146 	
147 	/**
148 	 * Updates this EventCountLookup object with a new Event.
149 	 * @param event The new Event.
150 	 */
151 	public void addEvent(Event event) {
152 		long userid = event.getUser().getId();
153 		InternalLookup lookup = _perUserLookupTable.get(userid);
154 		if (lookup==null) {
155 			_perUserLookupTable.put(userid, new InternalLookup(event));
156 		} else {
157 			lookup.addEvent(event);
158 		}
159 	}
160 
161 	/**
162 	 * Returns the amount of Events of a certain type.
163 	 * @param userid The user to look these Events up for.
164 	 * @param kind The type of Events to look the count up for.
165 	 * @return The amount of Events.
166 	 */
167 	public int getEventCount(long userid, int kind) {
168 		InternalLookup lookup = _perUserLookupTable.get(userid);
169 		if (lookup==null) {
170 			return 0;
171 		}
172 		switch (kind) {
173 			case Event.played : return lookup._playCount;
174 			case Event.downloaded : return lookup._downloadCount;
175 			case Event.requested : return lookup._requestCount;
176 			case Event.skipped : return lookup._skipCount;
177 			default : return 0;
178 		}
179 	}
180 
181 	/**
182 	 * Returns the amount of Events of a certain type, which took place in the
183 	 * last year.
184 	 * @param userid The user to look these Events up for.
185 	 * @param kind The type of Events to look the count up for.
186 	 * @return The amount of Events.
187 	 */
188 	public int getEventCountLastYear(long userid, int kind) {
189 		InternalLookup lookup = _perUserLookupTable.get(userid);
190 		if (lookup==null) {
191 			return 0;
192 		}
193 		switch (kind) {
194 			case Event.played : return lookup._playCountLastYear;
195 			case Event.downloaded : return lookup._downloadCountLastYear;
196 			case Event.requested : return lookup._requestCountLastYear;
197 			case Event.skipped : return lookup._skipCountLastYear;
198 			default : return 0;
199 		}
200 	}
201 
202 	/**
203 	 * Looks up the Date of the latest Play-Event.
204 	 * @param userid The user to look this Date up for.
205 	 * @return The Date when this User plays this song for the last time. ( Or 
206 	 *         null when never played.)
207 	 */
208 	public Date getLastPlay(long userid) {
209 		InternalLookup lookup = _perUserLookupTable.get(userid);
210 		if (lookup==null) {
211 			return null;
212 		}
213 		return lookup._lastPlay;
214 	}
215 
216 }