3300e1299e71b6c4268d9ba9447b65d6df17fe8f
[utils] / crawler / kiss / src / org / wamblee / crawler / kiss / main / KissCrawler.java
1 /*
2  * Copyright 2005 the original author or authors.
3  * 
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  * 
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  * 
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package org.wamblee.crawler.kiss.main;
18
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileNotFoundException;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import javax.mail.MessagingException;
30
31 import org.apache.commons.httpclient.HttpClient;
32 import org.apache.commons.httpclient.NameValuePair;
33 import org.apache.commons.logging.Log;
34 import org.apache.commons.logging.LogFactory;
35 import org.dom4j.Element;
36 import org.wamblee.crawler.Action;
37 import org.wamblee.crawler.Configuration;
38 import org.wamblee.crawler.Crawler;
39 import org.wamblee.crawler.Page;
40 import org.wamblee.crawler.PageException;
41 import org.wamblee.crawler.impl.ConfigurationParser;
42 import org.wamblee.crawler.impl.CrawlerImpl;
43 import org.wamblee.crawler.kiss.guide.Channel;
44 import org.wamblee.crawler.kiss.guide.PrintVisitor;
45 import org.wamblee.crawler.kiss.guide.Program;
46 import org.wamblee.crawler.kiss.guide.TVGuide;
47 import org.wamblee.crawler.kiss.guide.Time;
48 import org.wamblee.crawler.kiss.guide.TimeInterval;
49 import org.wamblee.crawler.kiss.notification.NotificationException;
50 import org.wamblee.crawler.kiss.notification.Notifier;
51 import org.wamblee.general.BeanFactory;
52 import org.wamblee.xml.ClasspathUriResolver;
53 import org.wamblee.xml.XslTransformer;
54
55 /**
56  * The KiSS crawler for automatic recording of interesting TV shows.
57  * 
58  */
59 public class KissCrawler {
60
61     private static final Log LOG = LogFactory.getLog(KissCrawler.class);
62
63     /**
64      * Start URL of the electronic programme guide.
65      */
66     private static final String START_URL = "http://epg.kml.kiss-technology.com/login.php";
67
68     /**
69      * Default socket timeout to use.
70      */
71     private static final int SOCKET_TIMEOUT = 10000;
72
73     /**
74      * Regular expression for matching time interval strings in the retrieved
75      * pages.
76      */
77     private static final String TIME_REGEX = "[^0-9]*([0-9]{2}):([0-9]{2})[^0-9]*([0-9]{2}):([0-9]{2}).*";
78
79     /**
80      * Compiled pattern for the time regular expression.
81      */
82     private Pattern _pattern;
83
84     /**
85      * Runs the KiSS crawler.
86      * 
87      * @param aArgs
88      *            Arguments, currently all ignored because they are hardcoded.
89      * @throws Exception
90      *             In case of problems.
91      */
92     public static void main(String[] aArgs) throws Exception {
93         String crawlerConfig = new File(aArgs[0]).getCanonicalPath();
94         String programConfig = new File(aArgs[1]).getCanonicalPath();
95
96         BeanFactory factory = new StandaloneCrawlerBeanFactory();
97         Notifier notifier = factory.find(Notifier.class);
98         new KissCrawler(START_URL, SOCKET_TIMEOUT, crawlerConfig,
99                 programConfig, notifier, new Report());
100     }
101
102     /**
103      * Constructs the crawler. This retrieves the TV guide by crawling the KiSS
104      * EPG guide, filters the guide for interesting programs, tries to record
105      * them, and sends a summary mail to the user.
106      * 
107      * @param aCrawlerConfig
108      *            Configuration file for the crawler.
109      * @param aProgramConfig
110      *            Configuration file describing interesting shows.
111      * @param aNotifier
112      *            Object used to send notifications of the results.
113      * @param aReport
114      *            Report to use.
115      * @throws IOException
116      *             In case of problems reading files.
117      * @throws NotificationException
118      *             In case notification fails.
119      * @throws PageException
120      *             In case of problems retrieving the TV guide.
121      */
122     public KissCrawler(String aCrawlerConfig, String aProgramConfig,
123             Notifier aNotifier, Report aReport) throws IOException,
124             NotificationException, PageException {
125         this(START_URL, SOCKET_TIMEOUT, aCrawlerConfig, aProgramConfig,
126                 aNotifier, aReport);
127     }
128
129     /**
130      * Constructs the crawler. This retrieves the TV guide by crawling the KiSS
131      * EPG guide, filters the guide for interesting programs, tries to record
132      * them, and sends a summary mail to the user.
133      * 
134      * @param aStartUrl
135      *            Start URL of the electronic programme guide.
136      * @param aSocketTimeout
137      *            Socket timeout to use.
138      * @param aCrawlerConfig
139      *            Configuration file for the crawler.
140      * @param aProgramConfig
141      *            Configuration file describing interesting shows.
142      * @param aNotifier
143      *            Object used to send notifications of the results.
144      * @param aReport
145      *            Report to use.
146      * @throws IOException
147      *             In case of problems reading files.
148      * @throws NotificationException
149      *             In case notification fails.
150      * @throws PageException
151      *             In case of problems retrieving the TV guide.
152      */
153     public KissCrawler(String aStartUrl, int aSocketTimeout,
154             String aCrawlerConfig, String aProgramConfig, Notifier aNotifier,
155             Report aReport) throws IOException, NotificationException,
156             PageException {
157
158         _pattern = Pattern.compile(TIME_REGEX);
159
160         try {
161             HttpClient client = new HttpClient();
162             // client.getHostConfiguration().setProxy("127.0.0.1", 3128);
163             client.getParams().setParameter("http.socket.timeout",
164                     SOCKET_TIMEOUT);
165
166             XslTransformer transformer = new XslTransformer(
167                     new ClasspathUriResolver());
168
169             Crawler crawler = createCrawler(aCrawlerConfig, client, transformer);
170             InputStream programConfigFile = new FileInputStream(new File(
171                     aProgramConfig));
172             ProgramConfigurationParser parser = new ProgramConfigurationParser();
173             parser.parse(programConfigFile);
174             List<ProgramFilter> programFilters = parser.getFilters();
175
176             try {
177                 Page page = getStartPage(aStartUrl, crawler, aReport);
178                 TVGuide guide = createGuide(page, aReport);
179                 PrintVisitor printer = new PrintVisitor(System.out);
180                 guide.accept(printer);
181                 processResults(programFilters, guide, aNotifier, aReport);
182             } catch (PageException e) {
183                 aReport.addMessage("Problem getting TV guide", e);
184                 LOG.info("Problem getting TV guide", e);
185                 throw e;
186             }
187             aNotifier.send(aReport.asXml());
188         } finally {
189             System.out.println("Crawler finished");
190         }
191     }
192
193     /**
194      * Records interesting shows.
195      * 
196      * @param aProgramCondition
197      *            Condition determining which shows are interesting.
198      * @param aGuide
199      *            Television guide.
200      * @throws MessagingException
201      *             In case of problems sending a summary mail.
202      */
203     private void processResults(List<ProgramFilter> aProgramCondition,
204             TVGuide aGuide, Notifier aNotifier, Report aReport) {
205         ProgramActionExecutor executor = new ProgramActionExecutor(aReport);
206         for (ProgramFilter filter : aProgramCondition) {
207             List<Program> programs = filter.apply(aGuide);
208             ProgramAction action = filter.getAction();
209             for (Program program : programs) {
210                 action.execute(program, executor);
211             }
212         }
213         executor.commit();
214
215     }
216
217     /**
218      * Creates the crawler.
219      * 
220      * @param aCrawlerConfig
221      *            Crawler configuration file.
222      * @param aOs
223      *            Logging output stream for the crawler.
224      * @param aClient
225      *            HTTP Client to use.
226      * @return Crawler.
227      * @throws FileNotFoundException
228      *             In case configuration files cannot be found.
229      */
230     private Crawler createCrawler(String aCrawlerConfig, HttpClient aClient,
231             XslTransformer aTransformer) throws FileNotFoundException {
232         ConfigurationParser parser = new ConfigurationParser(aTransformer);
233         InputStream crawlerConfigFile = new FileInputStream(new File(
234                 aCrawlerConfig));
235         Configuration config = parser.parse(crawlerConfigFile);
236         Crawler crawler = new CrawlerImpl(aClient, config);
237         return crawler;
238     }
239
240     /**
241      * Gets the start page of the electronic programme guide. This involves
242      * login and navigation to a suitable start page after logging in.
243      * 
244      * @param aStartUrl
245      *            URL of the electronic programme guide.
246      * @param aCrawler
247      *            Crawler to use.
248      * @param aReport
249      *            Report to use.
250      * @return Starting page.
251      */
252     private Page getStartPage(String aStartUrl, Crawler aCrawler, Report aReport)
253             throws PageException {
254         try {
255             Page page = aCrawler.getPage(aStartUrl, new NameValuePair[0]);
256             page = page.getAction("login").execute();
257             Action favorites = page.getAction("channels-favorites");
258             if (favorites == null) {
259                 String msg = "Channels favorites action not found on start page";
260                 throw new PageException(msg);
261             }
262             return favorites.execute();
263         } catch (PageException e) {
264             String msg = "Could not complete login to electronic programme guide.";
265             throw new PageException(msg, e);
266         }
267     }
268
269     /**
270      * Creates the TV guide by web crawling.
271      * 
272      * @param aPage
273      *            Starting page.
274      * @param aReport
275      *            Report to use.
276      * @return TV guide.
277      * @throws PageException
278      *             In case of problem getting the tv guide.
279      */
280     private TVGuide createGuide(Page aPage, Report aReport)
281             throws PageException {
282         LOG.info("Obtaining full TV guide");
283         Action[] actions = aPage.getActions();
284         if (actions.length == 0) {
285             LOG.error("No channels found");
286             throw new PageException("No channels found");
287         }
288         List<Channel> channels = new ArrayList<Channel>();
289         for (Action action : actions) {
290             try {
291                 LOG.info("Getting channel info for '" + action.getName() + "'");
292                 Action tomorrow = action.execute().getAction("tomorrow");
293                 if (tomorrow == null) {
294                     throw new PageException("Channel summary page for '"
295                             + action.getName()
296                             + "' does not contain required information");
297                 }
298                 Channel channel = createChannel(action.getName(), tomorrow
299                         .execute(), aReport);
300                 channels.add(channel);
301                 if (SystemProperties.isDebugMode()) {
302                     break; // Only one channel is crawled.
303                 }
304             } catch (PageException e) {
305                 aReport.addMessage("Could not create channel information for '"
306                         + action.getName() + "'");
307                 LOG.error("Could not create channel information for '"
308                         + action.getName() + "'", e);
309             }
310         }
311         return new TVGuide(channels);
312     }
313
314     /**
315      * Create channel information for a specific channel.
316      * 
317      * @param aChannel
318      *            Channel name.
319      * @param aPage
320      *            Starting page for the channel.
321      * @return Channel.
322      */
323     private Channel createChannel(String aChannel, Page aPage, Report aReport) {
324         LOG.info("Obtaining program for " + aChannel);
325         Action[] programActions = aPage.getActions();
326         List<Program> programs = new ArrayList<Program>();
327         for (Action action : programActions) {
328             String time = action.getContent().element("time").getText().trim();
329             Matcher matcher = _pattern.matcher(time);
330             if (matcher.matches()) {
331                 Time begin = new Time(Integer.parseInt(matcher.group(1)),
332                         Integer.parseInt(matcher.group(2)));
333                 Time end = new Time(Integer.parseInt(matcher.group(3)), Integer
334                         .parseInt(matcher.group(4)));
335                 TimeInterval interval = new TimeInterval(begin, end);
336                 String description = "";
337                 String keywords = "";
338
339                 if (!SystemProperties.isNoProgramDetailsRequired()) {
340                     Element descriptionElem = action.getContent().element(
341                             "description");
342                     if (descriptionElem == null) {
343                         try {
344                             Page programInfo = action.execute();
345                             description = programInfo.getContent().element(
346                                     "description").getText().trim();
347                             keywords = programInfo.getContent().element(
348                                     "keywords").getText().trim();
349                         } catch (PageException e) {
350                             String msg = "Program details could not be determined for '"
351                                     + action.getName() + "'";
352                             aReport.addMessage(msg, e);
353                             LOG.warn(msg, e);
354                         }
355                     } else {
356                         description = descriptionElem.getTextTrim();
357                     }
358                 }
359                 Program program = new Program(aChannel, action.getName(),
360                         description, keywords, interval, action);
361
362                 LOG.info("Got program " + program);
363                 programs.add(program);
364             }
365         }
366         return new Channel(aChannel, programs);
367     }
368 }