Java tutorial
/* * substitution-schedule-parser - Java library for parsing schools' substitution schedules * Copyright (c) 2016 Johan v. Forstner * * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package me.vertretungsplan.parser; import me.vertretungsplan.exception.CredentialInvalidException; import me.vertretungsplan.objects.Substitution; import me.vertretungsplan.objects.SubstitutionSchedule; import me.vertretungsplan.objects.SubstitutionScheduleData; import me.vertretungsplan.objects.SubstitutionScheduleDay; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.jsoup.Jsoup; import org.jsoup.nodes.*; import org.jsoup.select.Elements; import java.io.IOException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Contains common code used by {@link DSBLightParser}, {@link DSBMobileParser}, {@link UntisInfoParser}, * {@link UntisInfoHeadlessParser}, {@link UntisMonitorParser} and {@link UntisSubstitutionParser}. * * <h4>Configuration parameters</h4> * These parameters can be supplied in {@link SubstitutionScheduleData#setData(JSONObject)} in addition to the * parameters specified in the documentation of the parser itself. * * <dl> * <dt><code>columns</code> (Array of Strings, required)</dt> * <dd>The order of columns used in the substitutions table. Entries can be: <code>"lesson", "subject", * "previousSubject", "type", "type-entfall", "room", "previousRoom", "teacher", "previousTeacher", desc", * "desc-type", "substitutionFrom", "teacherTo", "class", "ignore"</code> (<code>"class"</code> only works when * <code>classInExtraLine</code> is <code>false</code>. * </dd> * * <dt><code>lastChangeLeft</code> (Boolean, optional)</dt> * <dd>Whether the date of last change is in the top left corner instead of in the <code>.mon_head</code> table. * Default: <code>false</code></dd> * * <dt><code>classInExtraLine</code> (Boolean, optional)</dt> * <dd>Whether the changes in the table are grouped using headers containing the class name(s). Default: * <code>false</code></dd> * * <dt><code>classesSeparated</code> (Boolean, optional)</dt> * <dd>Whether the class names are separated by commas. If this is set to <code>false</code>, combinations like "5abcde" * are attempted to be accounted for using an ugly algorithm based on RegExes generated from {@link #getAllClasses()}. * Default: <code>true</code></dd> * * <dt><code>excludeClasses</code> (Array of Strings, optional)</dt> * <dd>Substitutions for classes from this Array are ignored when reading the schedule. By default, only the class * <code>"-----"</code> is ignored.</dd> * * <dt><code>classRegex</code> (String, optional)</dt> * <dd>RegEx to modify the classes set on the schedule (in {@link #getSubstitutionSchedule()}, not * {@link #getAllClasses()}. The RegEx is matched against the class using {@link Matcher#find()}. If the RegEx * contains a group, the content of the first group {@link Matcher#group(int)} is used as the resulting class. * Otherwise, {@link Matcher#group()} is used. If the RegEx cannot be matched ({@link Matcher#find()} returns * <code>false</code>), the class is set to an empty string. * </dd> * * <dt><code>typeAutoDetection</code> (Boolean, optional)</dt> * <dd>If there is no type column and the detection using desc-type did not work, this sets whether the type may be * automatically set to "Entfall" (cancellation) depending on the values of other columns. * Default: <code>true</code></dd> * * </dl> */ public abstract class UntisCommonParser extends BaseParser { private static final String[] EXCLUDED_CLASS_NAMES = new String[] { "-----" }; private static final String PARAM_LAST_CHANGE_LEFT = "lastChangeLeft"; private static final String PARAM_LAST_CHANGE_SELECTOR = "lastChangeSelector"; // only used in UntisMonitorParser private static final String PARAM_CLASS_IN_EXTRA_LINE = "classInExtraLine"; private static final String PARAM_COLUMNS = "columns"; private static final String PARAM_CLASSES_SEPARATED = "classesSeparated"; private static final String PARAM_EXCLUDE_CLASSES = "excludeClasses"; private static final String PARAM_TYPE_AUTO_DETECTION = "typeAutoDetection"; private static final String PARAM_MERGE_WITH_DIFFERENT_TYPE = "mergeWithDifferentType"; UntisCommonParser(SubstitutionScheduleData scheduleData, CookieProvider cookieProvider) { super(scheduleData, cookieProvider); } static String findLastChange(Element doc, SubstitutionScheduleData scheduleData) { String lastChange = null; boolean lastChangeLeft = false; if (scheduleData != null) { if (scheduleData.getData().has("stand_links")) { // backwards compatibility lastChangeLeft = scheduleData.getData().optBoolean("stand_links", false); } else { lastChangeLeft = scheduleData.getData().optBoolean(PARAM_LAST_CHANGE_LEFT, false); } } if (doc.select("table.mon_head").size() > 0) { Element monHead = doc.select("table.mon_head").first(); lastChange = findLastChangeFromMonHeadTable(monHead); } else if (lastChangeLeft) { final String bodyHtml = doc.select("body").size() > 0 ? doc.select("body").html() : doc.html(); lastChange = bodyHtml.substring(0, bodyHtml.indexOf("<p>") - 1); } else { List<Node> childNodes; if (doc instanceof Document) { childNodes = ((Document) doc).body().childNodes(); } else { childNodes = doc.childNodes(); } for (Node node : childNodes) { if (node instanceof Comment) { Comment comment = (Comment) node; if (comment.getData().contains("<table class=\"mon_head\">")) { Document commentedDoc = Jsoup.parse(comment.getData()); Element monHead = commentedDoc.select("table.mon_head").first(); lastChange = findLastChangeFromMonHeadTable(monHead); break; } } } } return lastChange; } private static String findLastChangeFromMonHeadTable(Element monHead) { if (monHead.select("td[align=right]").size() == 0) return null; String lastChange = null; Pattern pattern = Pattern.compile("\\d\\d\\.\\d\\d\\.\\d\\d\\d\\d \\d\\d:\\d\\d"); Matcher matcher = pattern.matcher(monHead.select("td[align=right]").first().text()); if (matcher.find()) { lastChange = matcher.group(); } else if (monHead.text().contains("Stand: ")) { lastChange = monHead.text().substring(monHead.text().indexOf("Stand:") + "Stand:".length()).trim(); } return lastChange; } private static boolean equalsOrNull(String a, String b) { return a == null || b == null || a.equals(b); } /** * Parses an Untis substitution schedule table * * @param table the <code>table</code> Element from the HTML document * @param data {@link SubstitutionScheduleData#getData()} * @param day the {@link SubstitutionScheduleDay} where the substitutions will be stored */ void parseSubstitutionScheduleTable(Element table, JSONObject data, SubstitutionScheduleDay day) throws JSONException, CredentialInvalidException { parseSubstitutionScheduleTable(table, data, day, null); } /** * Parses an Untis substitution schedule table * * @param table the <code>table</code> Element from the HTML document * @param data {@link SubstitutionScheduleData#getData()} * @param day the {@link SubstitutionScheduleDay} where the substitutions will be stored * @param defaultClass the class that should be set if there is no class column in the table */ private void parseSubstitutionScheduleTable(Element table, JSONObject data, SubstitutionScheduleDay day, String defaultClass) throws JSONException, CredentialInvalidException { if (data.optBoolean(PARAM_CLASS_IN_EXTRA_LINE) || data.optBoolean("class_in_extra_line")) { // backwards compatibility for (Element element : table.select("td.inline_header")) { String className = getClassName(element.text(), data); if (isValidClass(className)) { Element zeile = null; try { zeile = element.parent().nextElementSibling(); if (zeile.select("td") == null) { zeile = zeile.nextElementSibling(); } int skipLines = 0; while (zeile != null && !zeile.select("td").attr("class").equals("list inline_header")) { if (skipLines > 0) { skipLines--; zeile = zeile.nextElementSibling(); continue; } Substitution v = new Substitution(); int i = 0; for (Element spalte : zeile.select("td")) { String text = spalte.text(); if (isEmpty(text)) { i++; continue; } int skipLinesForThisColumn = 0; Element nextLine = zeile.nextElementSibling(); boolean continueSkippingLines = true; while (continueSkippingLines) { if (nextLine != null && nextLine.children().size() == zeile.children().size()) { Element columnInNextLine = nextLine.child(spalte.elementSiblingIndex()); if (columnInNextLine.text().replaceAll("\u00A0", "").trim() .equals(nextLine.text().replaceAll("\u00A0", "").trim())) { // Continued in the next line text += " " + columnInNextLine.text(); skipLinesForThisColumn++; nextLine = nextLine.nextElementSibling(); } else { continueSkippingLines = false; } } else { continueSkippingLines = false; } } if (skipLinesForThisColumn > skipLines) skipLines = skipLinesForThisColumn; String type = data.getJSONArray(PARAM_COLUMNS).getString(i); switch (type) { case "lesson": v.setLesson(text); break; case "subject": handleSubject(v, spalte); break; case "previousSubject": v.setPreviousSubject(text); break; case "type": v.setType(text); v.setColor(colorProvider.getColor(text)); break; case "type-entfall": if (text.equals("x")) { v.setType("Entfall"); v.setColor(colorProvider.getColor("Entfall")); } else { v.setType("Vertretung"); v.setColor(colorProvider.getColor("Vertretung")); } break; case "room": handleRoom(v, spalte); break; case "teacher": handleTeacher(v, spalte, data); break; case "previousTeacher": v.setPreviousTeachers(splitTeachers(text, data)); break; case "desc": v.setDesc(text); break; case "desc-type": v.setDesc(text); String recognizedType = recognizeType(text); v.setType(recognizedType); v.setColor(colorProvider.getColor(recognizedType)); break; case "previousRoom": v.setPreviousRoom(text); break; case "substitutionFrom": v.setSubstitutionFrom(text); break; case "teacherTo": v.setTeacherTo(text); break; case "ignore": break; case "date": // used by UntisSubstitutionParser break; default: throw new IllegalArgumentException("Unknown column type: " + type); } i++; } autoDetectType(data, zeile, v); v.getClasses().add(className); if (v.getLesson() != null && !v.getLesson().equals("")) { day.addSubstitution(v); } zeile = zeile.nextElementSibling(); } } catch (Throwable e) { e.printStackTrace(); } } } } else { boolean hasType = false; for (int i = 0; i < data.getJSONArray(PARAM_COLUMNS).length(); i++) { if (data.getJSONArray(PARAM_COLUMNS).getString(i).equals("type")) { hasType = true; } } int skipLines = 0; for (Element zeile : table.select("tr.list.odd:not(:has(td.inline_header)), " + "tr.list.even:not(:has(td.inline_header)), " + "tr:has(td[align=center]):gt(0)")) { if (skipLines > 0) { skipLines--; continue; } Substitution v = new Substitution(); String klassen = defaultClass != null ? defaultClass : ""; int i = 0; for (Element spalte : zeile.select("td")) { String text = spalte.text(); String type = data.getJSONArray(PARAM_COLUMNS).getString(i); if (isEmpty(text) && !type.equals("type-entfall")) { i++; continue; } int skipLinesForThisColumn = 0; Element nextLine = zeile.nextElementSibling(); boolean continueSkippingLines = true; while (continueSkippingLines) { if (nextLine != null && nextLine.children().size() == zeile.children().size()) { Element columnInNextLine = nextLine.child(spalte.elementSiblingIndex()); if (columnInNextLine.text().replaceAll("\u00A0", "").trim() .equals(nextLine.text().replaceAll("\u00A0", "").trim())) { // Continued in the next line text += " " + columnInNextLine.text(); skipLinesForThisColumn++; nextLine = nextLine.nextElementSibling(); } else { continueSkippingLines = false; } } else { continueSkippingLines = false; } } if (skipLinesForThisColumn > skipLines) skipLines = skipLinesForThisColumn; switch (type) { case "lesson": v.setLesson(text); break; case "subject": handleSubject(v, spalte); break; case "previousSubject": v.setPreviousSubject(text); break; case "type": v.setType(text); v.setColor(colorProvider.getColor(text)); break; case "type-entfall": if (text.equals("x")) { v.setType("Entfall"); v.setColor(colorProvider.getColor("Entfall")); } else if (!hasType) { v.setType("Vertretung"); v.setColor(colorProvider.getColor("Vertretung")); } break; case "room": handleRoom(v, spalte); break; case "previousRoom": v.setPreviousRoom(text); break; case "desc": v.setDesc(text); break; case "desc-type": v.setDesc(text); String recognizedType = recognizeType(text); v.setType(recognizedType); v.setColor(colorProvider.getColor(recognizedType)); break; case "teacher": handleTeacher(v, spalte, data); break; case "previousTeacher": v.setPreviousTeachers(splitTeachers(text, data)); break; case "substitutionFrom": v.setSubstitutionFrom(text); break; case "teacherTo": v.setTeacherTo(text); break; case "class": klassen = getClassName(text, data); break; case "ignore": break; case "date": // used by UntisSubstitutionParser break; default: throw new IllegalArgumentException("Unknown column type: " + type); } i++; } if (v.getLesson() == null || v.getLesson().equals("")) { continue; } autoDetectType(data, zeile, v); List<String> affectedClasses; // Detect things like "7" Pattern singlePattern = Pattern.compile("(\\d+)"); Matcher singleMatcher = singlePattern.matcher(klassen); // Detect things like "5-12" Pattern rangePattern = Pattern.compile("(\\d+) ?- ?(\\d+)"); Matcher rangeMatcher = rangePattern.matcher(klassen); Pattern pattern2 = Pattern.compile("^(\\d+).*"); if (rangeMatcher.matches()) { affectedClasses = new ArrayList<>(); int min = Integer.parseInt(rangeMatcher.group(1)); int max = Integer.parseInt(rangeMatcher.group(2)); try { for (String klasse : getAllClasses()) { Matcher matcher2 = pattern2.matcher(klasse); if (matcher2.matches()) { int num = Integer.parseInt(matcher2.group(1)); if (min <= num && num <= max) affectedClasses.add(klasse); } } } catch (IOException e) { e.printStackTrace(); } } else if (singleMatcher.matches()) { affectedClasses = new ArrayList<>(); int grade = Integer.parseInt(singleMatcher.group(1)); try { for (String klasse : getAllClasses()) { Matcher matcher2 = pattern2.matcher(klasse); if (matcher2.matches() && grade == Integer.parseInt(matcher2.group(1))) { affectedClasses.add(klasse); } } } catch (IOException e) { e.printStackTrace(); } } else { if (data.optBoolean(PARAM_CLASSES_SEPARATED, true) && data.optBoolean("classes_separated", true)) { // backwards compatibility affectedClasses = Arrays.asList(klassen.split(", ")); } else { affectedClasses = new ArrayList<>(); try { for (String klasse : getAllClasses()) { // TODO: is there a better way? StringBuilder regex = new StringBuilder(); for (char character : klasse.toCharArray()) { if (character == '?') { regex.append("\\?"); } else { regex.append(character); } regex.append(".*"); } if (klassen.matches(regex.toString())) { affectedClasses.add(klasse); } } } catch (IOException e) { e.printStackTrace(); } } } for (String klasse : affectedClasses) { if (isValidClass(klasse)) { v.getClasses().add(klasse); } } if (data.optBoolean(PARAM_MERGE_WITH_DIFFERENT_TYPE, false)) { boolean found = false; for (Substitution subst : day.getSubstitutions()) { if (subst.equalsExcludingType(v)) { found = true; if (v.getType().equals("Vertretung")) { subst.setType("Vertretung"); subst.setColor(colorProvider.getColor("Vertretung")); } break; } } if (!found) { day.addSubstitution(v); } } else { day.addSubstitution(v); } } } } private void autoDetectType(JSONObject data, Element zeile, Substitution v) { if (v.getType() == null) { if (data.optBoolean(PARAM_TYPE_AUTO_DETECTION, true)) { if ((zeile.select("strike").size() > 0 && equalsOrNull(v.getSubject(), v.getPreviousSubject()) && equalsOrNull(v.getTeacher(), v.getPreviousTeacher())) || (v.getSubject() == null && v.getRoom() == null && v.getTeacher() == null && v.getPreviousSubject() != null)) { v.setType("Entfall"); v.setColor(colorProvider.getColor("Entfall")); } else { v.setType("Vertretung"); v.setColor(colorProvider.getColor("Vertretung")); } } else { v.setType("Vertretung"); v.setColor(colorProvider.getColor("Vertretung")); } } } static void handleTeacher(Substitution subst, Element cell, JSONObject data) { cell = getContentElement(cell); if (cell.select("s").size() > 0) { subst.setPreviousTeachers(splitTeachers(cell.select("s").text(), data)); if (cell.ownText().length() > 0) { subst.setTeachers( splitTeachers(cell.ownText().replaceFirst("^\\?", "").replaceFirst("", ""), data)); } } else { subst.setTeachers(splitTeachers(cell.text(), data)); } } private static Set<String> splitTeachers(String s, JSONObject data) { Set<String> teachers = new HashSet<>(); if (data.optBoolean("splitTeachers", true)) { teachers.addAll(Arrays.asList(s.split(", "))); } else { teachers.add(s); } return teachers; } static void handleRoom(Substitution subst, Element cell) { cell = getContentElement(cell); if (cell.select("s").size() > 0) { subst.setPreviousRoom(cell.select("s").text()); if (cell.ownText().length() > 0) { subst.setRoom(cell.ownText().replaceFirst("^\\?", "").replaceFirst("", "")); } } else { subst.setRoom(cell.text()); } } private static Element getContentElement(Element cell) { if (cell.ownText().isEmpty() && cell.select("> span").size() == 1) { cell = cell.select("> span").first(); } return cell; } static void handleSubject(Substitution subst, Element cell) { cell = getContentElement(cell); if (cell.select("s").size() > 0) { subst.setPreviousSubject(cell.select("s").text()); if (cell.ownText().length() > 0) { subst.setSubject(cell.ownText().replaceFirst("^\\?", "").replaceFirst("", "")); } } else { subst.setSubject(cell.text()); } } private boolean isEmpty(String text) { return text.replaceAll("\u00A0", "").trim().equals("") || text.replaceAll("\u00A0", "").trim().equals("---"); } /** * Parses a "Nachrichten zum Tag" ("daily news") table from an Untis schedule * * @param table the <code>table</code>-Element to be parsed * @param day the {@link SubstitutionScheduleDay} where the messages should be stored */ private void parseMessages(Element table, SubstitutionScheduleDay day) { Elements zeilen = table.select("tr:not(:contains(Nachrichten zum Tag))"); for (Element i : zeilen) { Elements spalten = i.select("td"); String info = ""; for (Element b : spalten) { info += "\n" + TextNode.createFromEncoded(b.html(), null).getWholeText(); } info = info.substring(1); // remove first \n day.addMessage(info); } } SubstitutionScheduleDay parseMonitorDay(Element doc, JSONObject data) throws JSONException, CredentialInvalidException { SubstitutionScheduleDay day = new SubstitutionScheduleDay(); String date = doc.select(".mon_title").first().text().replaceAll(" \\(Seite \\d+ / \\d+\\)", ""); day.setDateString(date); day.setDate(ParserUtils.parseDate(date)); if (!scheduleData.getData().has(PARAM_LAST_CHANGE_SELECTOR)) { String lastChange = findLastChange(doc, scheduleData); day.setLastChangeString(lastChange); day.setLastChange(ParserUtils.parseDateTime(lastChange)); } // NACHRICHTEN if (doc.select("table.info").size() > 0) { parseMessages(doc.select("table.info").first(), day); } // VERTRETUNGSPLAN if (doc.select("table:has(tr.list)").size() > 0) { parseSubstitutionScheduleTable(doc.select("table:has(tr.list)").first(), data, day); } return day; } private boolean isValidClass(String klasse) throws JSONException { return klasse != null && !Arrays.asList(EXCLUDED_CLASS_NAMES).contains(klasse) && !(scheduleData.getData().has(PARAM_EXCLUDE_CLASSES) && contains(scheduleData.getData().getJSONArray(PARAM_EXCLUDE_CLASSES), klasse)) && !(scheduleData.getData().has("exclude_classes") && // backwards compatibility contains(scheduleData.getData().getJSONArray("exclude_classes"), klasse)); } @Override public List<String> getAllClasses() throws IOException, JSONException, CredentialInvalidException { return getClassesFromJson(); } void parseDay(SubstitutionScheduleDay day, Element next, SubstitutionSchedule v, String klasse) throws JSONException, CredentialInvalidException { if (next.className().equals("subst") || next.select(".list").size() > 0 || next.text().contains("Vertretungen sind nicht freigegeben") || next.text().contains("Keine Vertretungen")) { //Vertretungstabelle if (next.text().contains("Vertretungen sind nicht freigegeben")) { return; } parseSubstitutionScheduleTable(next, scheduleData.getData(), day, klasse); } else { //Nachrichten parseMessages(next, day); next = next.nextElementSibling().nextElementSibling(); parseSubstitutionScheduleTable(next, scheduleData.getData(), day, klasse); } v.addDay(day); } void parseMultipleMonitorDays(SubstitutionSchedule v, Document doc, JSONObject data) throws JSONException, CredentialInvalidException { if (doc.select(".mon_head").size() > 1) { for (int j = 0; j < doc.select(".mon_head").size(); j++) { Document doc2 = Document.createShell(doc.baseUri()); doc2.body().appendChild(doc.select(".mon_head").get(j).clone()); Element next = doc.select(".mon_head").get(j).nextElementSibling(); if (next != null && next.tagName().equals("center")) { doc2.body().appendChild(next.select(".mon_title").first().clone()); if (next.select("table:has(tr.list)").size() > 0) { doc2.body().appendChild(next.select("table:has(tr.list)").first()); } if (next.select("table.info").size() > 0) { doc2.body().appendChild(next.select("table.info").first()); } } else if (doc.select(".mon_title").size() - 1 >= j) { doc2.body().appendChild(doc.select(".mon_title").get(j).clone()); doc2.body().appendChild(doc.select("table:has(tr.list)").get(j).clone()); } else { continue; } SubstitutionScheduleDay day = parseMonitorDay(doc2, data); v.addDay(day); } } else if (doc.select(".mon_title").size() > 1) { for (int j = 0; j < doc.select(".mon_title").size(); j++) { Document doc2 = Document.createShell(doc.baseUri()); doc2.body().appendChild(doc.select(".mon_title").get(j).clone()); Element next = doc.select(".mon_title").get(j).nextElementSibling(); while (next != null && !next.tagName().equals("center")) { doc2.body().appendChild(next); next = doc.select(".mon_title").get(j).nextElementSibling(); } SubstitutionScheduleDay day = parseMonitorDay(doc2, data); v.addDay(day); } } else { SubstitutionScheduleDay day = parseMonitorDay(doc, data); v.addDay(day); } } /** * Parses an Untis substitution table ({@link UntisSubstitutionParser}). * * @param v * @param lastChange * @param doc * @throws JSONException * @throws CredentialInvalidException */ protected void parseSubstitutionTable(SubstitutionSchedule v, String lastChange, Document doc) throws JSONException, CredentialInvalidException { JSONObject data = scheduleData.getData(); LocalDateTime lastChangeDate = ParserUtils.parseDateTime(lastChange); Pattern dayPattern = Pattern.compile("\\d\\d?.\\d\\d?. / \\w+"); int dateColumn = -1; JSONArray columns = data.getJSONArray("columns"); for (int i = 0; i < columns.length(); i++) { if (columns.getString(i).equals("date")) { dateColumn = i; break; } } Element table = doc.select("table[rules=all], table:has(tr:has(td[align=center]))").first(); if (table.text().replace("\u00a0", "").trim().equals("Keine Vertretungen")) return; if (dateColumn == -1) { SubstitutionScheduleDay day = new SubstitutionScheduleDay(); day.setLastChangeString(lastChange); day.setLastChange(lastChangeDate); String title = doc.select("font[size=5], font[size=4], font[size=3] b").text(); Matcher matcher = dayPattern.matcher(title); if (matcher.find()) { String date = matcher.group(); day.setDateString(date); day.setDate(ParserUtils.parseDate(date)); } parseSubstitutionScheduleTable(table, data, day); v.addDay(day); } else { for (Element line : table.select("tr.list.odd:not(:has(td.inline_header)), " + "tr.list.even:not(:has(td.inline_header)), " + "tr:has(td[align=center]):gt(0)")) { SubstitutionScheduleDay day = null; String date = line.select("td").get(dateColumn).text().trim(); if (date.indexOf("-") > 0) { date = date.substring(0, date.indexOf("-") - 1).trim(); } LocalDate parsedDate = ParserUtils.parseDate(date); for (SubstitutionScheduleDay search : v.getDays()) { if (Objects.equals(search.getDate(), parsedDate) || Objects.equals(search.getDateString(), date)) { day = search; break; } } if (day == null) { day = new SubstitutionScheduleDay(); day.setDateString(date); day.setDate(parsedDate); day.setLastChangeString(lastChange); day.setLastChange(lastChangeDate); v.addDay(day); } parseSubstitutionScheduleTable(line, data, day); } } } }