package de.saxsys.projectiler.crawler.jsoup; import java.io.IOException; import java.net.UnknownHostException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.logging.Logger; import org.jsoup.Connection; import org.jsoup.Connection.Method; import org.jsoup.Connection.Response; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import de.saxsys.projectiler.crawler.Booking; import de.saxsys.projectiler.crawler.ConnectionException; import de.saxsys.projectiler.crawler.Crawler; import de.saxsys.projectiler.crawler.CrawlingException; import de.saxsys.projectiler.crawler.Credentials; import de.saxsys.projectiler.crawler.InvalidCredentialsException; import de.saxsys.projectiler.crawler.NoBudgetException; import de.saxsys.projectiler.crawler.OverlapException; import de.saxsys.projectiler.crawler.Settings; /** * Interacts with Projectile via JSoup. * * @author stefan.bley * @see <a href="http://jsoup.org">http://jsoup.org</a> */ public class JSoupCrawler implements Crawler { private static final Logger LOGGER = Logger.getLogger(JSoupCrawler.class.getSimpleName()); private static final String JSESSIONID = "JSESSIONID"; /** select box value for 'Heute' */ private static final String TODAY = "4"; private final Settings settings; /** transaction id */ private String taid; private Map<String, String> cookies; public JSoupCrawler(final Settings settings) { this.settings = settings; } @Override public void checkCredentials(final Credentials credentials) throws CrawlingException { try { Document startPage = login(credentials); logout(startPage); } catch (final UnknownHostException e) { throw new ConnectionException(e); } catch (final IOException e) { throw new CrawlingException("Error while checking credentials.", e); } } @Override public List<String> getProjectNames(final Credentials credentials) throws CrawlingException { try { Document startPage = login(credentials); Document ttPage = openTimeTracker(startPage); List<String> projectNames = readProjectNames(ttPage); logout(ttPage); return projectNames; } catch (final UnknownHostException e) { throw new ConnectionException(e); } catch (final IOException e) { throw new CrawlingException("Error while retrieving project names.", e); } } @Override public void clock(final Credentials credentials, final String projectName, final Date start, final Date end, final String comment) throws CrawlingException { try { Document startPage = login(credentials); Document ttPage = openTimeTracker(startPage); clockTime(start, end, projectName, comment, ttPage); logout(ttPage); } catch (final UnknownHostException e) { throw new ConnectionException(e); } catch (final IOException e) { throw new CrawlingException("Error while clocking time.", e); } } @Override public List<Booking> getDailyReport(final Credentials credentials) throws ConnectionException, CrawlingException { try { Document startPage = login(credentials); Document ttPage = openTimeTracker(startPage); List<Booking> bookings = readDailyReport(ttPage); logout(ttPage); return bookings; } catch (final UnknownHostException e) { throw new ConnectionException(e); } catch (final IOException e) { throw new CrawlingException("Error while retrieving daily report.", e); } } /** * Login to Projectile and return the cookies containing the session ID. * * @throws InvalidCredentialsException * if the credentials are wrong */ private Document login(final Credentials cred) throws IOException, InvalidCredentialsException { Response response = jsoupConnection().data("login", cred.getUsername()) .data("password", cred.getPassword()) .data("jsenabled", "0") .data("external.loginOK.x", "8") .data("external.loginOK.y", "8") .execute(); Document startPage = response.parse(); if (startPage.getElementsByAttributeValue("name", "password").isEmpty()) { String sessionId = response.cookie(JSESSIONID); LOGGER.info("User " + cred.getUsername() + " logged in with session ID " + sessionId + "."); saveTaid(startPage); cookies = response.cookies(); return startPage; } else { throw new InvalidCredentialsException(); } } private Document openTimeTracker(final Document startPage) throws IOException { Document currentPage = startPage; boolean autostart = !startPage.select("input[name$=.Field.0.Autostart]").isEmpty(); if (autostart) { currentPage = openIntroPage(currentPage); } Document introPage = openStandardIntroPage(currentPage); String today = formatToday(); Response response = jsoupConnection().cookies(cookies) .data("taid", taid) .data("CurrentFocusField", "0") .data("CurrentDraggable", "0") .data("CurrentDropTraget", "0") .data(introPage.select("input[name$=.Button=TimeTracker1").first().attr("name") + ".x", "8") .data(introPage.select("input[name$=.Button=TimeTracker1").first().attr("name") + ".y", "8") .data(introPage.select("input[name$=.val.BsmHiddenViewField]").first().attr("name"), "1") .data(introPage.select("select[name$=.Field.0.Field_TimeTracker").first().attr("name"), TODAY) .data(introPage.select("input[name$=.Field.0.Field_TimeTrackerDate:0").first().attr("name"), today) .data(introPage.select("input[name$=.Field.0.Field_TimeTrackerDate2:0").first().attr("name"), today) .execute(); Document ttPage = response.parse(); saveTaid(ttPage); return ttPage; } private Document openIntroPage(Document startPage) throws IOException { Response response = jsoupConnection().cookies(cookies) .data("taid", taid) .data("CurrentFocusField", "0") .data("CurrentDraggable", "0") .data("CurrentDropTraget", "0") .data(startPage.select("input[name$=.BUTTON.intro").first().attr("name") + ".x", "8") .data(startPage.select("input[name$=.BUTTON.intro").first().attr("name") + ".y", "8") .data(startPage.select("input[name$=.val.BsmHiddenViewField]").first().attr("name"), "1") .execute(); Document introPage = response.parse(); saveTaid(introPage); return introPage; } private Document openStandardIntroPage(final Document startPage) throws IOException { Response response = jsoupConnection().cookies(cookies) .data("taid", taid) .data("CurrentFocusField", "0") .data("CurrentDraggable", "0") .data("CurrentDropTraget", "0") .data(startPage.select("input[name$=.BUTTON.SelectTab.0").first().attr("name"), "0") .data(startPage.select("input[name$=.val.BsmHiddenViewField]").first().attr("name"), "1") .execute(); Document ttPage = response.parse(); saveTaid(ttPage); return ttPage; } private List<String> readProjectNames(final Document timeTrackerPage) throws IOException, CrawlingException { List<String> projectNames = new ArrayList<String>(); Elements options = timeTrackerPage.select("select[id$=NewWhat_0_0] option"); if (options.isEmpty()) { throw new CrawlingException("No projects found."); } for (Element option : options) { String text = option.text(); if (!text.trim().isEmpty()) { projectNames.add(text); } } LOGGER.info("Project names read."); return projectNames; } private void clockTime(final Date start, final Date end, final String projectName, final String comment, final Document ttPage) throws IOException, CrawlingException { String optionValue = null; Elements options = ttPage.select("select[id$=NewWhat_0_0] option"); for (Element option : options) { if (projectName.equals(option.text())) { optionValue = option.val(); break; } } String nullSafeComment = (null == comment) ? "" : comment; final Response response = jsoupConnection().cookies(cookies) .data("taid", taid) .data("CurrentFocusField", "name") .data("CurrentDraggable", "0") .data("CurrentDropTraget", "0") .data(ttPage.select("input[name$=.val.BsmHiddenViewField]").first().attr("name"), "1") .data(ttPage.select("input[name$=+0+0__ButtonTable_]").first().attr("name") + ".x", "8") .data(ttPage.select("input[name$=+0+0__ButtonTable_]").first().attr("name") + ".y", "8") .data(ttPage.select("select[id$=.Field.0.TimeTrackerConfirmationAction").first().id(), "-1") .data(ttPage.select("input[id$=Field.0.Begin:0]").first().id(), formatToday()) .data(ttPage.select("input.rw[id$=NewFrom_0_0]").first().id(), formatTime(start)) .data(ttPage.select("input.rw[id$=NewTo_0_0]").first().id(), formatTime(end)) .data(ttPage.select("input.rw[id$=NewTime_0_0]").first().id(), "") .data(ttPage.select("select[id$=NewWhat_0_0]").first().id(), optionValue) .data(ttPage.select("input.rw[id$=NewNote_0_0]").first().id(), nullSafeComment) .execute(); Document document = response.parse(); saveTaid(document); // verify time has been clocked if (!document.select("td:contains(wird überschrieben)").isEmpty()) { LOGGER.info("Booking overlaps an existing booking."); throw new OverlapException(); } if (!document.select("td:contains(darf keine weitere Zeit)").isEmpty()) { LOGGER.info("No budget left for project '" + projectName + "'."); throw new NoBudgetException(); } boolean clocked = false; try { String inputStartId = document.select("input.rw[id*=Field_Start][value=" + formatTime(start) + "]") .first() .id(); String inputEndId = inputStartId.replace("Start", "End"); clocked = !document.select("input.rw[id=" + inputEndId + "][value=" + formatTime(end) + "]").isEmpty(); String inputWhatId = inputStartId.replace("Start", "What"); clocked &= !document.select("select[id=" + inputWhatId + "] option[selected][value=" + optionValue + "]") .isEmpty(); clocked &= document.select("img[src*=error_i1_16.gif]").isEmpty(); } catch (NullPointerException e) { clocked = false; } finally { if (!clocked) { LOGGER.info("Time has not been clocked for project '" + projectName + "'."); throw new CrawlingException("Time has not been clocked."); } } LOGGER.info("Time clocked for project '" + projectName + "'."); } private List<Booking> readDailyReport(final Document timeTrackerPage) { List<Booking> bookings = new ArrayList<Booking>(); final Elements inputsStart = timeTrackerPage.select("input.rw[id*=Field_Start]"); for (Element inputStart : inputsStart) { String inputStartId = inputStart.id(); String startTime = inputStart.val(); String inputEndId = inputStartId.replace("Start", "End"); String endTime = timeTrackerPage.select("input.rw[id=" + inputEndId + "]").first().val(); String inputWhatId = inputStartId.replace("Start", "What"); String projectName = timeTrackerPage.select("select[id=" + inputWhatId + "] option[selected]") .first() .text(); bookings.add(new Booking(projectName, startTime, endTime)); } LOGGER.info("Daily report read."); return bookings; } private void logout(final Document page) throws IOException { Response execute = jsoupConnection().cookies(cookies) .data("taid", taid) .data(page.select("input[name$=L.BUTTON.logout]").first().attr("name") + ".x", "8") .data(page.select("input[name$=L.BUTTON.logout]").first().attr("name") + ".y", "8") .execute(); final Elements select = execute.parse().select("td:contains(korrekt beendet)"); if (select.isEmpty()) { LOGGER.severe("Error while logging out."); } else { LOGGER.info("User logged out."); } } /** * Creates a JSoup connection to Projectile with method POST and given timeout */ private Connection jsoupConnection() { return Jsoup.connect(settings.getProjectileUrl()).timeout(settings.getTimeout()).method(Method.POST); } /** Reads the transaction ID from the response and stores it to a field */ private void saveTaid(final Document page) { taid = page.select("input[name=taid]").val(); LOGGER.finer("TAID: " + taid); } /** Time formatted to Projectile format */ private String formatTime(final Date time) { return new SimpleDateFormat("HH:mm").format(time); } /** Current date formatted as dd.MM.yyyy */ private String formatToday() { return new SimpleDateFormat("dd.MM.yyyy").format(new Date()); } }