| summaryrefslogtreecommitdiff |
diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/CAMSIParser.java | 125 | ||||
| -rw-r--r-- | src/Calendar.java | 39 | ||||
| -rw-r--r-- | src/CelcatParser.java | 482 | ||||
| -rw-r--r-- | src/Classes.java | 341 | ||||
| -rw-r--r-- | src/DHXCalParser.java | 263 | ||||
| -rw-r--r-- | src/Error.java | 196 | ||||
| -rw-r--r-- | src/Event.java | 286 | ||||
| -rw-r--r-- | src/Group.java | 560 | ||||
| -rw-r--r-- | src/ICSParser.java | 99 | ||||
| -rw-r--r-- | src/Parameters.java | 135 |
10 files changed, 2526 insertions, 0 deletions
diff --git a/src/CAMSIParser.java b/src/CAMSIParser.java new file mode 100644 index 0000000..b074b16 --- /dev/null +++ b/src/CAMSIParser.java @@ -0,0 +1,125 @@ +import java.util.TimeZone; +import java.util.Calendar; +import java.util.Locale; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import java.io.BufferedReader; + +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamException; + +/** + * This class stores the incoming data from XML. + * It is needed because of the stream nature of the XML parser. + **/ +public class CAMSIParser +{ + final static Pattern event_pattern; + + static + { + event_pattern = Pattern.compile + ( + "[^<]*<td><[^>]*>([0-9]+)/([0-9]+)/([0-9]+)</[^>]*></td>" + + "<td><[^>]*>([0-9]+):([0-9]+)</[^>]*></td>" + + "<td><[^>]*>([0-9]+):([0-9]+)</[^>]*></td>" + + "<td><[^>]*>([^<]*)</[^>]*></td>" // 8th + + "<td><[^>]*>([^<]*)</[^>]*></td>" + + "<td><[^>]*>([^<]*)</[^>]*></td>" + ); + } + + public static void parse + ( + final BufferedReader reader, + final Group group, + final String uid_prefix + ) + throws Exception + { + int uid; + String line; + Matcher matcher; + + uid = 0; + + while (reader.ready()) + { + line = reader.readLine(); + matcher = event_pattern.matcher(line); + + if (matcher.matches()) + { + final Event e; + final java.util.Calendar c_start, c_end; + + c_start = java.util.Calendar.getInstance + ( + TimeZone.getTimeZone("Europe/Paris"), + Locale.FRENCH + ); + + c_start.set + ( + java.util.Calendar.DAY_OF_MONTH, + Integer.parseInt(matcher.group(1)) + ); + + c_start.set + ( + java.util.Calendar.MONTH, + /* It's Java... don't question it. */ + (Integer.parseInt(matcher.group(2)) - 1) + ); + + c_start.set + ( + java.util.Calendar.YEAR, + Integer.parseInt(matcher.group(3)) + ); + + c_start.set + ( + java.util.Calendar.HOUR_OF_DAY, + Integer.parseInt(matcher.group(4)) + ); + + c_start.set + ( + java.util.Calendar.MINUTE, + Integer.parseInt(matcher.group(5)) + ); + + + c_end = (java.util.Calendar) c_start.clone(); + + c_end.set + ( + java.util.Calendar.HOUR_OF_DAY, + Integer.parseInt(matcher.group(6)) + ); + + c_end.set + ( + java.util.Calendar.MINUTE, + Integer.parseInt(matcher.group(7)) + ); + + e = new Event(); + + e.set_start_time(c_start); + e.set_end_time(c_end); + e.set_name(matcher.group(8)); + e.add_speaker(matcher.group(9)); + e.add_location(matcher.group(10)); + e.set_uid(uid_prefix + (uid++)); + + group.add_event(e); + } + } + + reader.close(); + } +} diff --git a/src/Calendar.java b/src/Calendar.java new file mode 100644 index 0000000..18c097d --- /dev/null +++ b/src/Calendar.java @@ -0,0 +1,39 @@ +import java.util.ArrayList; +import java.util.List; +import java.util.HashMap; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.InputStreamReader; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +public class Calendar +{ + private Calendar () {} /* Utility class */ + + public static void main (final String[] args) + { + if (!Parameters.set_parameters(args)) + { + /** + * Printed usage. + **/ + return; + } + + Error.init_handler(); + + try + { + Classes.read_all(); + Group.read_all(); + Group.run_all(); + } + catch (final Exception e) {} + + Error.finalize_handler(); + } +} diff --git a/src/CelcatParser.java b/src/CelcatParser.java new file mode 100644 index 0000000..01e0314 --- /dev/null +++ b/src/CelcatParser.java @@ -0,0 +1,482 @@ +import java.util.HashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Calendar; +import java.util.Locale; +import java.util.Iterator; +import java.util.TimeZone; + +import java.io.PrintWriter; + +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamException; + +/** + * This class stores the incoming data from XML. + * It is needed because of the stream nature of the XML parser. + **/ +public class CelcatParser +{ + private final HashMap<Integer, Calendar> weeks; + private final Group group; + private String day; + private String week; + private String start_time; + private String end_time; + private String uid_prefix; + private Event event; + private DataType data_type; + private ListType current_list_type; + private int event_count; + + public static void parse + ( + final XMLStreamReader stream_reader, + final Group group, + final String uid_prefix + ) + throws XMLStreamException + { + final CelcatParser parser; + + parser = new CelcatParser(group, uid_prefix); + + while (stream_reader.hasNext()) + { + switch (stream_reader.next()) + { + case XMLStreamReader.START_ELEMENT: + parser.handle_new_element(stream_reader); + break; + + case XMLStreamReader.CDATA: + case XMLStreamReader.CHARACTERS: + parser.handle_data(stream_reader.getText().trim()); + break; + + case XMLStreamReader.END_DOCUMENT: + parser.flush(); + break; + + default: + break; + } + } + } + + private CelcatParser + ( + final Group group, + final String uid_prefix + ) + { + this.group = group; + this.uid_prefix = uid_prefix; + + event = null; + event_count = 0; + weeks = new HashMap<Integer, Calendar>(); + } + + /** + * Called when entering an new element. + **/ + private void handle_new_element (final XMLStreamReader stream_reader) + { + final DataType new_data_type; + + new_data_type = DataType.get(stream_reader.getLocalName()); + + switch (new_data_type) + { + case EVENT: + /** We're done handling the previous event. **/ + flush(); + break; + + case SPAN: + /** This contains information about the weeks. **/ + weeks.put + ( + Integer.parseInt + ( + stream_reader.getAttributeValue(null, "rawix") + ), + get_calendar_from_span + ( + stream_reader.getAttributeValue(null, "date") + ) + ); + break; + + case ROOM: + /** + * Multiple rooms may be concerned, hence the List. + * Any following ITEM elements should be considered as describing + * a room for this event. + **/ + current_list_type = ListType.ROOMS; + break; + + case GROUP: + /** + * Multiple groups may be concerned, hence the List. + * Any following ITEM elements should be considered as describing + * a group for this event. + **/ + current_list_type = ListType.GROUPS; + break; + + case ITEM: + if (data_type == DataType.MODULE) + { + /** + * The XML file will consider that the event may concern + * multiple modules. It makes sense, but I've never seen + * it happen and it complicates the process. + * Not updating the data_type here will make the next + * data be considered as the Module's name instead of + * something to add to a list. + **/ + + /* [XXX][Improvement] Handle it. */ + + return; + } + break; + + default: + break; + } + + data_type = new_data_type; + } + + private void flush () + { + week = null; + start_time = null; + end_time = null; + + if (event == null) + { + /** + * This is the first event, we don't have anything to write. + **/ + + event = new Event(); + event.set_uid(uid_prefix + (event_count++)); + + return; + } + + group.add_event(event); + + event = new Event(); + event.set_uid(uid_prefix + (event_count++)); + + current_list_type = ListType.IGNORED; + } + + /** + * Called when finding actual data. + **/ + private void handle_data (final String data) + { + if (data.equals("")) + { + /** + * Data was garbage. + * It happens sometimes, I don't know why. + **/ + + /* [XXX][Improvement] Prevent this from happening. */ + return; + } + + /** + * Uses the previously set "data_type" to know where the data belongs. + **/ + switch (data_type) + { + case MODULE: + event.set_name(data); + break; + + case STARTTIME: + if ((week == null) || (day == null)) + { + start_time = data; + } + else + { + event.set_start_time + ( + get_calendar + ( + week_to_calendar(week), + day, + data + ) + ); + } + break; + + case ENDTIME: + if ((week == null) || (day == null)) + { + end_time = data; + } + else + { + event.set_end_time + ( + get_calendar + ( + week_to_calendar(week), + day, + data + ) + ); + } + break; + + case NOTES: + if (event != null) + { + event.set_description("[Notes] " + data); + } + break; + + case RAWWEEKS: + week = data; + + if ((start_time != null) && (day != null)) + { + event.set_start_time + ( + get_calendar + ( + week_to_calendar(week), + day, + start_time + ) + ); + } + + if ((end_time != null) && (day != null)) + { + event.set_end_time + ( + get_calendar + ( + week_to_calendar(week), + day, + end_time + ) + ); + } + + break; + + case DAY: + day = data; + + if ((start_time != null) && (week != null)) + { + event.set_start_time + ( + get_calendar + ( + week_to_calendar(week), + day, + start_time + ) + ); + } + + if ((end_time != null) && (week != null)) + { + event.set_end_time + ( + get_calendar + ( + week_to_calendar(week), + day, + end_time + ) + ); + } + break; + + case CATEGORY: + event.set_category(data); + break; + + case ITEM: + switch (current_list_type) + { + case ROOMS: + event.add_location(data); + break; + + case GROUPS: + event.add_attendee(data); + break; + + default: + break; + } + break; + } + } + + /** + * Retrieves the relevant data from a week description. + **/ + private Calendar get_calendar_from_span (final String spandata) + { + final String[] data; + final Calendar result; + + data = spandata.split("/"); + + result = + java.util.Calendar.getInstance + ( + TimeZone.getTimeZone("Europe/Paris"), + Locale.FRENCH + ); + + if (data.length != 3) + { + Error.ERROR.from_thread + ( + group.get_abbreviation(), + ("Invalid date format for week:" + spandata) + ); + + return null; + } + + result.set(java.util.Calendar.DAY_OF_MONTH, Integer.parseInt(data[0])); + + /* Java... don't ask... */ + result.set(java.util.Calendar.MONTH, Integer.parseInt(data[1]) - 1); + + result.set(java.util.Calendar.YEAR, Integer.parseInt(data[2])); + + return result; + } + + + /** + * Retrieves the week relevant to the event being processed. + **/ + private Calendar week_to_calendar + ( + final String week + ) + { + final java.util.Calendar result; + int pos; + + for (pos = 0; week.charAt(pos) != 'Y'; ++pos); + + result = weeks.get(pos + 1); + + if (result == null) + { + Error.ERROR.from_thread + ( + group.get_abbreviation(), + "An event appeared before its week description did." + ); + } + + return result; + } + + private java.util.Calendar get_calendar + ( + final Calendar week, + final String day, + final String hourmin + ) + { + final String[] time; + final java.util.Calendar result; + + time = hourmin.split(":"); + + result = (java.util.Calendar) week.clone(); + + result.set(java.util.Calendar.HOUR_OF_DAY, Integer.parseInt(time[0])); + result.set(java.util.Calendar.MINUTE, Integer.parseInt(time[1])); + result.add(java.util.Calendar.DAY_OF_YEAR, Integer.parseInt(day)); + + return result; + } + + private static enum ListType + { + IGNORED, ROOMS, GROUPS; + } + + /** + * This enum is used to sort what is relevant in the XML file. + * Sadly, for justified reasons, Java does not permit access to the static + * members of an enum from its constructor, hence the "static"'s redundancy. + **/ + private static enum DataType + { + IRRELEVANT, + EVENT, + DAY, + STARTTIME, + ENDTIME, + CATEGORY, + RAWWEEKS, + ITEM, + MODULE, + ROOM, + SPAN, + NOTES, + GROUP; + + private static final HashMap<String, DataType> FROM_STRING; + + static + { + FROM_STRING = new HashMap<String, DataType>(); + + FROM_STRING.put("irrelevant", IRRELEVANT); + FROM_STRING.put("event", EVENT); + FROM_STRING.put("day", DAY); + FROM_STRING.put("starttime", STARTTIME); + FROM_STRING.put("endtime", ENDTIME); + FROM_STRING.put("category", CATEGORY); + FROM_STRING.put("item", ITEM); + FROM_STRING.put("module", MODULE); + FROM_STRING.put("room", ROOM); + FROM_STRING.put("group", GROUP); + FROM_STRING.put("rawweeks", RAWWEEKS); + FROM_STRING.put("span", SPAN); + FROM_STRING.put("notes", NOTES); + } + + public static DataType get (final String e) + { + final DataType result; + + result = FROM_STRING.get(e); + + if (result == null) + { + return IRRELEVANT; + } + + return result; + } + } +} diff --git a/src/Classes.java b/src/Classes.java new file mode 100644 index 0000000..31cc0fa --- /dev/null +++ b/src/Classes.java @@ -0,0 +1,341 @@ +import java.util.List; +import java.util.Set; +import java.util.HashMap; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.InputStreamReader; + +public class Classes +{ + private static final HashMap<String, Classes> FROM_ABBREVIATION; + private static final HashMap<String, Classes> FROM_NAME; + private static final String SEPARATOR = " ,.?-&:;"; + + private final String name; + private final String abbreviation; + + static + { + FROM_ABBREVIATION = new HashMap<String, Classes>(); + FROM_NAME = new HashMap<String, Classes>(); + } + + private Classes (final String name, final String abbreviation) + { + this.name = name; + this.abbreviation = abbreviation; + } + + private void register () + throws Classes.AlreadyRegisteredException + { + /** + * We allow multiple (unique) names for the same class, but not + * multiple abbreviations + **/ + + if (FROM_NAME.containsKey(name)) + { + throw (new Classes.AlreadyRegisteredException()); + } + + if (!FROM_ABBREVIATION.containsKey(abbreviation)) + { + FROM_ABBREVIATION.put(abbreviation, this); + } + + FROM_NAME.put(name, this); + } + + @Override + public boolean equals (Object a) + { + if (a instanceof Classes) + { + return abbreviation.equals(((Classes) a).abbreviation); + } + + return false; + } + + @Override + public String toString () + { + return abbreviation; + } + + public static String name_from_abbreviation (final String abbr) + { + final Classes c; + + c = FROM_ABBREVIATION.get(abbr); + + if (c == null) + { + return "unknown"; + } + + return c.name; + } + + private static boolean word_is_isolated + ( + final String word, + final String sentence + ) + { + final int index_start, index_end; + + index_start = sentence.indexOf(word); + + if + ( + (index_start == -1) + || (index_start == 0) + || (SEPARATOR.indexOf(sentence.charAt(index_start - 1)) == -1) + ) + { + return false; + } + + index_end = (index_start + word.length()); + + if (index_end == sentence.length()) + { + return true; + } + + return (SEPARATOR.indexOf(sentence.charAt(index_end)) != -1); + } + + private static Classes ambiguous_entry_guard + ( + final String entry, + final String class_name + ) + { + final String rest_of_entry_name; + boolean ambiguous; + + ambiguous = false; + rest_of_entry_name = entry.substring(class_name.length()); + + for (final String other_class_name: FROM_NAME.keySet()) + { + if + ( + !other_class_name.equals(class_name) + && word_is_isolated(other_class_name, rest_of_entry_name) + && !FROM_NAME.get(class_name).equals + ( + FROM_NAME.get(other_class_name) + ) + ) + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("Ambiguous event summary \""); + sb.append(entry); + sb.append("\". Starts with \""); + sb.append(class_name); + sb.append("\" ("); + sb.append(FROM_NAME.get(class_name)); + sb.append("), but also contains \""); + sb.append(other_class_name); + sb.append("\" ("); + sb.append(FROM_NAME.get(other_class_name)); + sb.append(")"); + + Error.WARNING.from_file + ( + Parameters.get_known_classes_filename(), + sb.toString() + ); + + ambiguous = true; + + break; + } + } + + return ambiguous ? null : FROM_NAME.get(class_name); + } + public static String abbreviation_from_name + ( + final String name, + final boolean lazy + ) + { + Classes c; + + if (lazy) + { + c = null; + + for (final String n: FROM_NAME.keySet()) + { + if (name.startsWith(n)) + { + c = ambiguous_entry_guard(name, n); + + break; + } + } + } + else + { + c = FROM_NAME.get(name); + } + + if (c == null) + { + return "unknown"; + } + + return c.abbreviation; + } + + public static Set<String> get_all_abbreviations () + { + return FROM_ABBREVIATION.keySet(); + } + + public static void read_all () + throws Exception + { + BufferedReader br; + String input_line; + String[] data; + Classes new_class; + + br = null; + + try + { + br = + new BufferedReader + ( + new InputStreamReader + ( + new FileInputStream(Parameters.get_known_classes_filename()) + ) + ); + + while ((input_line = br.readLine()) != null) + { + data = input_line.split("::"); + + if (data.length != 3) + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("Invalid class entry: \""); + sb.append(input_line); + + Error.ERROR.from_file + ( + Parameters.get_known_classes_filename(), + sb.toString() + ); + + continue; + } + + new_class = new Classes(data[1], data[2]); + + if (!data[0].equals("") && FROM_ABBREVIATION.containsKey(data[2])) + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("Abbreviation \""); + sb.append(data[2]); + sb.append("\" is already in use."); + + Error.ERROR.from_file + ( + Parameters.get_known_classes_filename(), + sb.toString() + ); + + continue; + } + + try + { + new_class.register(); + } + catch (final Classes.AlreadyRegisteredException care) + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("Duplicate entry for class \""); + sb.append(data[1]); + sb.append("\"."); + + Error.ERROR.from_file + ( + Parameters.get_known_classes_filename(), + sb.toString() + ); + + continue; + } + } + } + catch (final Exception e) + { + Error.FATAL.from_file + ( + Parameters.get_known_classes_filename(), + "Error while reading class file:", + e.getMessage() + ); + + if (br != null) + { + try + { + br.close(); + } + catch (final Exception e2) + { + Error.WARNING.from_file + ( + Parameters.get_known_classes_filename(), + "Error while closing class file:", + e2.getMessage() + ); + } + } + + throw e; + } + + try + { + br.close(); + } + catch (final Exception e) + { + Error.WARNING.from_file + ( + Parameters.get_known_classes_filename(), + "Error while closing class file:", + e.getMessage() + ); + + throw e; + } + } + + private static class AlreadyRegisteredException extends Exception {} +} diff --git a/src/DHXCalParser.java b/src/DHXCalParser.java new file mode 100644 index 0000000..bad0581 --- /dev/null +++ b/src/DHXCalParser.java @@ -0,0 +1,263 @@ +import java.util.HashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Calendar; +import java.util.Locale; +import java.util.Iterator; +import java.util.TimeZone; + +import java.io.PrintWriter; + +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamException; + +/** + * This class stores the incoming data from XML. + * It is needed because of the stream nature of the XML parser. + **/ +public class DHXCalParser +{ + private final Group group; + private String uid_prefix; + private Event event; + private DataType data_type; + private int event_count; + + public static void parse + ( + final XMLStreamReader stream_reader, + final Group group, + final String uid_prefix + ) + throws XMLStreamException + { + final DHXCalParser parser; + + parser = new DHXCalParser(group, uid_prefix); + + while (stream_reader.hasNext()) + { + switch (stream_reader.next()) + { + case XMLStreamReader.START_ELEMENT: + parser.handle_new_element(stream_reader); + break; + + case XMLStreamReader.CDATA: + case XMLStreamReader.CHARACTERS: + parser.handle_data(stream_reader.getText().trim()); + break; + + case XMLStreamReader.END_DOCUMENT: + parser.flush(); + break; + + default: + break; + } + } + } + + private DHXCalParser + ( + final Group group, + final String uid_prefix + ) + { + this.group = group; + this.uid_prefix = uid_prefix; + + event = null; + event_count = 0; + } + + /** + * Called when entering an new element. + **/ + private void handle_new_element (final XMLStreamReader stream_reader) + { + final DataType new_data_type; + + new_data_type = DataType.get(stream_reader.getLocalName()); + + switch (new_data_type) + { + case EVENT: + /** We're done handling the previous event. **/ + flush(); + break; + + default: + break; + } + + data_type = new_data_type; + } + + private void flush () + { + if (event == null) + { + /** + * This is the first event, we don't have anything to write. + **/ + + event = new Event(); + event.set_uid(uid_prefix + (event_count++)); + + return; + } + + group.add_event(event); + + event = new Event(); + event.set_uid(uid_prefix + (event_count++)); + } + + /** + * Called when finding actual data. + **/ + private void handle_data (final String data) + { + final String[] info; + final StringBuilder sb; + int i; + + if (data.equals("")) + { + /** + * Data was garbage. + * It happens sometimes, I don't know why. + **/ + + /* [XXX][Improvement] Prevent this from happening. */ + return; + } + + /** + * Uses the previously set "data_type" to know where the data belongs. + **/ + switch (data_type) + { + case START_DATE: + event.set_start_time(date_to_calendar(data)); + break; + + case END_DATE: + event.set_end_time(date_to_calendar(data)); + break; + + case TEXT: + info = + data.replaceAll + ( + "<b>|</b>", + "" + ).replaceAll + ( + "\\s+", + " " + ).split("<br />"); + + event.set_name(info[0]); + + sb = new StringBuilder(); + + for (i = 1; i < info.length; ++i) + { + sb.append(info[i].trim()); + sb.append(", "); + } + + if (info.length > 1) + { + sb.delete((sb.length() - 2), sb.length()); + } + + event.set_description(sb.toString()); + break; + + case DETAILS: + // TODO + break; + + default: + break; + } + } + + private static Calendar date_to_calendar (final String data) + { + final String[] day_hour; + final String[] hour; + final String[] day; + final java.util.Calendar result; + + day_hour = data.split(" "); + hour = day_hour[1].split(":"); + day = day_hour[0].split("-"); + + result = + java.util.Calendar.getInstance + ( + TimeZone.getTimeZone("Europe/Paris"), + Locale.FRENCH + ); + + result.set(java.util.Calendar.DAY_OF_MONTH, Integer.parseInt(day[2])); + + /* Java... don't ask... */ + result.set(java.util.Calendar.MONTH, Integer.parseInt(day[1]) - 1); + result.set(java.util.Calendar.YEAR, Integer.parseInt(day[0])); + + result.set(java.util.Calendar.HOUR_OF_DAY, Integer.parseInt(hour[0])); + result.set(java.util.Calendar.MINUTE, Integer.parseInt(hour[1])); + + return result; + } + + /** + * This enum is used to sort what is relevant in the XML file. + * Sadly, for justified reasons, Java does not permit access to the static + * members of an enum from its constructor, hence the "static"'s redundancy. + **/ + private static enum DataType + { + IRRELEVANT, + EVENT, + START_DATE, + END_DATE, + TEXT, + DETAILS, + DATA; + + private static final HashMap<String, DataType> FROM_STRING; + + static + { + FROM_STRING = new HashMap<String, DataType>(); + + FROM_STRING.put("irrelevant", IRRELEVANT); + FROM_STRING.put("event", EVENT); + FROM_STRING.put("start_date", START_DATE); + FROM_STRING.put("end_date", END_DATE); + FROM_STRING.put("text", TEXT); + FROM_STRING.put("details", DETAILS); + FROM_STRING.put("data", DATA); + } + + public static DataType get (final String e) + { + final DataType result; + + result = FROM_STRING.get(e); + + if (result == null) + { + return IRRELEVANT; + } + + return result; + } + } +} diff --git a/src/Error.java b/src/Error.java new file mode 100644 index 0000000..2f40e4b --- /dev/null +++ b/src/Error.java @@ -0,0 +1,196 @@ +import java.util.concurrent.Semaphore; + +import java.io.PrintWriter; +import java.io.FileWriter; + +public enum Error +{ + WARNING("[W]"), + ERROR("[E]"), + FATAL("[F]"), + PROGRAM("[P]"); /* Errors that should not if the code's logic was correct. */ + + private static final Semaphore MUTEX; + private static PrintWriter OUTPUT; + private final String emblem; + private int count; + + static + { + MUTEX = new Semaphore(1); + } + + private Error (final String emblem) + { + this.emblem = emblem; + this.count = 0; + } + + public static int init_handler () + { + try + { + OUTPUT = + new PrintWriter + ( + new FileWriter(Parameters.get_log_filename()) + ); + } + catch (final Exception e) + { + System.err.println("[F] Could not init the Error Handler:"); + e.printStackTrace(); + + return -1; + } + + return 0; + } + + public static void finalize_handler () + { + OUTPUT.flush(); + OUTPUT.close(); + + summary(); + } + + public void from_thread + ( + final String thread_id, + final String error + ) + { + try + { + MUTEX.acquire(); + + OUTPUT.print(emblem); + OUTPUT.print("[Thread: "); + OUTPUT.print(thread_id); + OUTPUT.print("] "); + OUTPUT.println(error); + + ++count; + + MUTEX.release(); + } + catch (final InterruptedException ie) + { + System.err.println + ( + "[W] An thread was interrupted while trying to report an error." + ); + } + } + + public void from_thread + ( + final String thread_id, + final String error, + final String report + ) + { + try + { + MUTEX.acquire(); + + OUTPUT.print(emblem); + OUTPUT.print("[Thread: "); + OUTPUT.print(thread_id); + OUTPUT.print("] "); + OUTPUT.println(error); + OUTPUT.println(report); + + ++count; + + MUTEX.release(); + } + catch (final InterruptedException ie) + { + System.err.println + ( + "[W] An thread was interrupted while trying to report an error." + ); + } + } + + public void from_file + ( + final String filename, + final String error + ) + { + try + { + MUTEX.acquire(); + + OUTPUT.print(emblem); + OUTPUT.print("[File: "); + OUTPUT.print(filename); + OUTPUT.print("] "); + OUTPUT.println(error); + + ++count; + + MUTEX.release(); + } + catch (final InterruptedException ie) + { + System.err.println + ( + "[W] An thread was interrupted while trying to report an error." + ); + } + } + + public void from_file + ( + final String filename, + final String error, + final String report + ) + { + try + { + MUTEX.acquire(); + + OUTPUT.print(emblem); + OUTPUT.print("[File: "); + OUTPUT.print(filename); + OUTPUT.print("] "); + OUTPUT.println(error); + OUTPUT.println(report); + + ++count; + + MUTEX.release(); + } + catch (final InterruptedException ie) + { + System.err.println + ( + "[W] An thread was interrupted while trying to report an error." + ); + } + } + + private static void summary () + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("Log report:\n"); + + for (final Error e: Error.values()) + { + sb.append(e.emblem); + sb.append(" "); + sb.append(e.count); + sb.append("\n"); + } + + System.out.println(sb.toString()); + } +} diff --git a/src/Event.java b/src/Event.java new file mode 100644 index 0000000..4a56475 --- /dev/null +++ b/src/Event.java @@ -0,0 +1,286 @@ +import java.util.ArrayList; +import java.util.List; + +import java.util.Locale; +import java.util.Calendar; +import java.util.TimeZone; + +public class Event +{ + private final List<String> locations; + private final List<String> speakers; + private final List<String> attendees; + private String name; + private String category; + private String uid; + private String description; + private java.util.Calendar start_time; + private java.util.Calendar end_time; + private java.util.Calendar creation_time; + + public Event () + { + locations = new ArrayList<String>(); + speakers = new ArrayList<String>(); + attendees = new ArrayList<String>(); + + name = null; + category = null; + description = null; + start_time = null; + end_time = null; + + creation_time = + java.util.Calendar.getInstance + ( + TimeZone.getTimeZone("Europe/Paris"), + Locale.FRENCH + ); + } + + public String get_name () + { + return name; + } + + public void set_name (final String val) + { + if (val != null) + { + name = + val.trim + ( + ).replaceAll + ( + "(?<!\\\\),", + "\\\\," + ).replaceAll + ( + "(?<!\\\\);", + "\\\\;" + ); + } + } + + public void set_description (final String val) + { + if (val != null) + { + description = + val.replaceAll + ( + "(?<!\\\\),", + "\\\\," + ).replaceAll + ( + "(?<!\\\\);", + "\\\\;" + ); + } + } + + public void set_category (final String val) + { + if (val != null) + { + category = val; + } + } + + public void set_uid (final String val) + { + if (val != null) + { + uid = val; + } + } + + public void set_start_time (final java.util.Calendar val) + { + if (val != null) + { + start_time = val; + } + } + + public void set_end_time (final java.util.Calendar val) + { + if (val != null) + { + end_time = val; + } + } + + public void add_location (final String str) + { + locations.add(str); + } + + public void add_speaker (final String str) + { + speakers.add(str); + } + + public void add_attendee (final String str) + { + attendees.add(str); + } + + public String toString() + { + final StringBuilder sb; + final String creation_time_str; + + sb = new StringBuilder(); + + creation_time_str = get_time(creation_time); + + sb.append("BEGIN:VEVENT"); + + sb.append("\r\nSUMMARY:"); + + if (category != null) + { + sb.append("["); + sb.append(category); + sb.append("] "); + } + + if (name != null) + { + sb.append(name); + } + + if (!locations.isEmpty()) + { + sb.append("\r\nLOCATION:"); + + for (final String s: locations) + { + sb.append(s); + sb.append("\\n"); + } + + sb.delete(sb.length() - 2, sb.length()); + } + + sb.append("\r\nDESCRIPTION:"); + + if (description != null) + { + sb.append(description); + + if + ( + !speakers.isEmpty() + || !attendees.isEmpty() + ) + { + sb.append("\\n"); + } + } + if (!speakers.isEmpty()) + { + + for (final String s: speakers) + { + sb.append("[Speaker] "); + sb.append(s); + sb.append("\\n"); + } + + if (attendees.isEmpty()) + { + sb.delete(sb.length() - 2, sb.length()); + } + } + + if (!attendees.isEmpty()) + { + + for (final String s: attendees) + { + sb.append("[Attendee] "); + sb.append(s); + sb.append("\\n"); + } + + sb.delete(sb.length() - 2, sb.length()); + } + + + sb.append("\r\nUID:"); + sb.append(uid); + + sb.append("\r\nCREATED:"); + sb.append(creation_time_str); + + sb.append("\r\nLAST-MODIFIED:"); + sb.append(creation_time_str); + + sb.append("\r\nDTSTART:"); + sb.append(get_time(start_time)); + + sb.append("\r\nDTEND:"); + sb.append(get_time(end_time)); + + sb.append("\r\nSTATUS:CONFIRMED"); + + sb.append("\r\nSEQUENCE:0"); + + sb.append("\r\nTRANSP:OPAQUE"); + + sb.append("\r\nEND:VEVENT"); + + return sb.toString(); + } + + private String get_time (final java.util.Calendar cal) + { + final StringBuilder sb; + int val; + + sb = new StringBuilder(); + + sb.append(cal.get(java.util.Calendar.YEAR)); + + val = cal.get(java.util.Calendar.MONTH) + 1; /* Java... don't ask... */ + + if (val < 10) + { + sb.append("0"); + } + + sb.append(val); + + append_two_digit(sb, cal, java.util.Calendar.DAY_OF_MONTH); + + sb.append("T"); + + append_two_digit(sb, cal, java.util.Calendar.HOUR_OF_DAY); + append_two_digit(sb, cal, java.util.Calendar.MINUTE); + + sb.append("00"); + + return sb.toString(); + } + + private void append_two_digit + ( + final StringBuilder sb, + final java.util.Calendar cal, + final int val_id + ) + { + final int val; + + val = cal.get(val_id); + + if (val < 10) + { + sb.append("0"); + } + + sb.append(val); + } +} diff --git a/src/Group.java b/src/Group.java new file mode 100644 index 0000000..3e1a1f2 --- /dev/null +++ b/src/Group.java @@ -0,0 +1,560 @@ +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +import java.io.BufferedReader; +import java.io.PrintWriter; +import java.io.FileWriter; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.io.IOException; + +import java.net.URL; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamReader; + +public class Group extends Thread +{ + private static final HashMap<String, Group> KNOWN_GROUPS; + private final HashMap<String, PrintWriter> class_files; + private final String name; + private final String abbreviation; + private final String[] url; + private final Group.Type[] type; + + static + { + KNOWN_GROUPS = new HashMap<String, Group>(); + } + + private Group + ( + final String name, + final String abbreviation, + final String[] types_and_urls + ) + { + final int sources; + int j; + + sources = (types_and_urls.length / 2); + + this.name = name; + this.abbreviation = abbreviation; + + url = new String[sources]; + type = new Group.Type[sources]; + + for (int i = 0; i < sources; ++i) + { + j = (i * 2); + type[i] = Type.get(types_and_urls[j]); + url[i] = types_and_urls[j + 1]; + } + + class_files = new HashMap<String, PrintWriter>(); + } + + private void register () + throws Group.AlreadyRegisteredException + { + if (KNOWN_GROUPS.containsKey(abbreviation)) + { + throw (new Group.AlreadyRegisteredException()); + } + + KNOWN_GROUPS.put(abbreviation, this); + } + + public String get_abbreviation () + { + return abbreviation; + } + + public void add_event (final Event event) + { + final PrintWriter pw; + + pw = + class_files.get + ( + Classes.abbreviation_from_name + ( + event.get_name(), + false + ) + ); + + pw.println(event.toString()); + pw.flush(); + } + + public void add_ICS_fragment + ( + final String fragment, + final String class_name, + final boolean lazy + ) + { + final PrintWriter pw; + + pw = + class_files.get + ( + Classes.abbreviation_from_name(class_name, lazy) + ); + + // Escapes all ',' or ';' not directly preceded by '\'. + // That's quite an inefficient way to do it btw. + pw.print + ( + fragment.replaceAll + ( + "(?<!\\\\),", + "\\\\," + ).replaceAll + ( + "(?<!\\\\);", + "\\\\;" + ) + ); + + pw.flush(); + } + + private static enum Type + { + CELCAT(true), + DHX_CAL(true), + ICS(false), + LAZY_ICS(false), + ICS_NOUID(false), + LAZY_ICS_NOUID(false), + CAMSI(false); + + public final boolean uses_xml; + + private Type (final boolean uses_xml) + { + this.uses_xml = uses_xml; + } + + public static Type get (final String str) + { + if (str.equals("celcat")) + { + return CELCAT; + } + else if (str.equals("dhx_cal")) + { + return DHX_CAL; + } + else if (str.equals("ics_nouid")) + { + return ICS_NOUID; + } + else if (str.equals("lazy_ics_nouid")) + { + return LAZY_ICS_NOUID; + } + else if (str.equals("ics")) + { + return ICS; + } + else if (str.equals("lazy_ics")) + { + return LAZY_ICS; + } + else if (str.equals("camsi")) + { + return CAMSI; + } + + return null; + } + } + + private boolean create_class_files () + { + try + { + for (final String abbr: Classes.get_all_abbreviations()) + { + class_files.put + ( + abbr, + new PrintWriter + ( + new FileWriter + ( + Parameters.get_output_directory() + + "/" + + abbreviation + + "_" + + abbr + + ".ics" + ) + ) + ); + } + + class_files.put + ( + "unknown", + new PrintWriter + ( + new FileWriter + ( + Parameters.get_output_directory() + + "/" + + abbreviation + + "_unknown.ics" + ) + ) + ); + } + catch (final IOException e) + { + Error.ERROR.from_thread + ( + abbreviation, + ("Could not create output file: " + e.toString()) + ); + + return false; + } + + return true; + } + + private void finalize_class_files () + { + for (final PrintWriter pw: class_files.values()) + { + pw.flush(); + pw.close(); + } + } + + private InputStreamReader remove_fuckups + ( + final InputStreamReader irs + ) + throws Exception + { + final BufferedReader in; + final PrintWriter pw; + final String filename; + String input; + boolean first_line; + + filename = + ( + Parameters.get_output_directory() + + "/" + + abbreviation + + ".fuckup" + ); + + in = new BufferedReader(irs); + pw = new PrintWriter(filename); + + first_line = true; + + while ((input = in.readLine()) != null) + { + if (!input.equals("")) + { + if (first_line) + { + pw.println(input.trim().replaceFirst("^([\\W]+)<","<")); + first_line = false; + } + else + { + pw.println(input); + } + } + } + + pw.close(); + irs.close(); + + return (new InputStreamReader(new FileInputStream(filename))); + } + + private void parse_source + ( + final int id, + final Group.Type type, + final String url + ) + throws Exception + { + final XMLInputFactory input_factory; + final XMLStreamReader stream_reader; + final StringBuilder sb; + + if (type.uses_xml) + { + input_factory = XMLInputFactory.newInstance(); + stream_reader = + input_factory.createXMLStreamReader + ( + remove_fuckups(new InputStreamReader((new URL(url)).openStream(), "UTF-8")) + ); + sb = new StringBuilder(); + + sb.append(abbreviation); + sb.append("_s"); + sb.append(id); + sb.append("_event"); + } + else + { + stream_reader = null; + sb = null; + } + + switch (type) + { + case CELCAT: + CelcatParser.parse(stream_reader, this, sb.toString()); + break; + + case DHX_CAL: + DHXCalParser.parse(stream_reader, this, sb.toString()); + break; + + case ICS_NOUID: + ICSParser.parse(url, this, false, true); + break; + + case LAZY_ICS_NOUID: + ICSParser.parse(url, this, true, true); + break; + + case ICS: + ICSParser.parse(url, this, false, false); + break; + + case LAZY_ICS: + ICSParser.parse(url, this, true, false); + break; + + case CAMSI: + CAMSIParser.parse + ( + new BufferedReader + ( + new InputStreamReader + ( + (new URL(url)).openStream() + ) + ), + this, + (abbreviation + "_s" + id + "_event") + ); + break; + } + } + + @Override + public void run () + { + + if (!create_class_files()) + { + return; + } + + for (int i = 0; i < type.length; ++i) + { + try + { + parse_source(i, type[i], url[i]); + } + catch (final Exception e) + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("An error occured while parsing source '"); + sb.append(url[i]); + sb.append("'"); + + Error.ERROR.from_thread + ( + abbreviation, + sb.toString(), + e.getMessage() + ); + } + } + + finalize_class_files(); + } + + public static void read_all () + { + BufferedReader br; + String input_line; + String[] data; + Group new_group; + + br = null; + + try + { + br = + new BufferedReader + ( + new InputStreamReader + ( + new FileInputStream(Parameters.get_groups_filename()) + ) + ); + + while ((input_line = br.readLine()) != null) + { + data = input_line.split("::"); + + if (data.length >= 4 && ((data.length % 2) == 0)) + { + new_group = + new Group + ( + data[0], + data[1], + Arrays.copyOfRange(data, 2, data.length) + ); + + try + { + new_group.register(); + } + catch (final Group.AlreadyRegisteredException e) + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("Duplicate group entry for group \""); + sb.append(new_group.name); + sb.append("\"."); + + Error.ERROR.from_file + ( + Parameters.get_groups_filename(), + sb.toString() + ); + + continue; + } + } + else + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append("Invalid group entry: \""); + sb.append(input_line); + sb.append("\"."); + + Error.ERROR.from_file + ( + Parameters.get_groups_filename(), + sb.toString() + ); + + continue; + } + } + } + catch (final Exception e) + { + Error.FATAL.from_file + ( + Parameters.get_groups_filename(), + "Error while reading file:", + e.getMessage() + ); + + if (br != null) + { + try + { + br.close(); + } + catch (final Exception e2) + { + Error.WARNING.from_file + ( + Parameters.get_groups_filename(), + "Error while closing file:", + e2.getMessage() + ); + } + } + + return; + } + + try + { + br.close(); + } + catch (final Exception e) + { + Error.WARNING.from_file + ( + Parameters.get_groups_filename(), + "Error while closing file:", + e.getMessage() + ); + } + } + + /** + * Creates Group threads (one per group) to translate each XML file. + * For performance reasons, we limit the number of threads running + * concurrently at a give time (also, I'm on shared hosting and the task + * doesn't actually need to be very fast so I avoid being too greedy). + **/ + public static void run_all () + { + final ExecutorService exec; + final List<Callable<Object>> groups; + + groups = new ArrayList<Callable<Object>>(); + + exec = Executors.newFixedThreadPool(Parameters.get_max_threads()); + + for (final Group g: KNOWN_GROUPS.values()) + { + groups.add(Executors.callable(g)); + } + + /** This won't return until all is done. **/ + try + { + exec.invokeAll(groups); + } + catch (final InterruptedException ie) + { + Error.FATAL.from_thread("main", "Interrupted before the end."); + } + + exec.shutdown(); + } + + private static class AlreadyRegisteredException extends Exception {} +} diff --git a/src/ICSParser.java b/src/ICSParser.java new file mode 100644 index 0000000..ac4f96d --- /dev/null +++ b/src/ICSParser.java @@ -0,0 +1,99 @@ +import java.io.BufferedReader; +import java.io.InputStreamReader; + +import java.security.MessageDigest; + +import java.net.URL; + +public class ICSParser +{ + + public static void parse + ( + final String url, + final Group group, + final boolean lazy, + final boolean uid_override + ) + throws Exception + { + final BufferedReader reader; + final StringBuilder sb; + + boolean should_record, has_id, ignore_line; + String line; + String class_name; + String hash_seed; + int length; + sb = new StringBuilder(); + reader = + new BufferedReader(new InputStreamReader((new URL(url)).openStream())); + + class_name = "unknown"; + should_record = false; + ignore_line = false; + + hash_seed = ""; + has_id = false; + + while ((line = reader.readLine()) != null) + { + if (line.startsWith("END:VEVENT")) + { + if (!has_id) + { + sb.append("UID:noid_"); + sb.append(hash_seed.hashCode()); + sb.append("\r\n"); + } + + sb.append(line); + sb.append("\r\n"); + + should_record = false; + + group.add_ICS_fragment(sb.toString(), class_name, lazy); + sb.setLength(0); + } + else if (line.startsWith("BEGIN:VEVENT")) + { + should_record = true; + has_id = false; + hash_seed = ""; + } + else if (line.startsWith("DTSTART:")) + { + hash_seed += line; + } + else if (line.startsWith("SUMMARY:")) + { + class_name = line.substring(8, line.length()); + hash_seed += line; + } + else if (line.startsWith("UID")) + { + if (uid_override) + { + ignore_line = true; + } + else + { + has_id = true; + } + } + + if (line.matches("\\w+:.*") && should_record) + { + if (!ignore_line) + { + sb.append(line); + sb.append("\r\n"); + } + + ignore_line = false; + } + } + + reader.close(); + } +} diff --git a/src/Parameters.java b/src/Parameters.java new file mode 100644 index 0000000..d323908 --- /dev/null +++ b/src/Parameters.java @@ -0,0 +1,135 @@ +public class Parameters +{ + private static String output_dir; + private static String log_filename, known_classes_filename, groups_filename; + private static int threads; + + static + { + output_dir = "../output/"; + log_filename = "../last_log"; + known_classes_filename = "../data/KNOWN_CLASSES"; + groups_filename = "../data/GROUPS"; + threads = 8; + } + + private Parameters () {} /** utility class **/ + + public static boolean set_parameters (final String[] args) + { + int i; + + for (i = 0; i < args.length; i++) + { + if(args[i].equals("--outputs")) + { + i++; + output_dir = args[i]; + } + else if(args[i].equals("--log")) + { + i++; + log_filename = args[i]; + } + else if(args[i].equals("--classes")) + { + i++; + known_classes_filename = args[i]; + } + else if(args[i].equals("--groups")) + { + i++; + groups_filename = args[i]; + } + else if(args[i].equals("--threads")) + { + i++; + threads = Integer.parseInt(args[i]); + } + else if(args[i].equals("--help") || args[i].equals("-h")) + { + usage(); + + return false; + } + else + { + System.err.println("[W] Unknown parameter:" + args[i]); + } + } + + return true; + } + + private static void usage () + { + final StringBuilder sb; + + sb = new StringBuilder(); + + sb.append + ( + ").\n\"--outputs directory\": where the 'ICS' files are written " + + "(default: " + ); + sb.append(output_dir); + + sb.append + ( + ").\n\"--log filename\": where the errors are reported (default: " + ); + sb.append(log_filename); + + sb.append + ( + ").\n\"--classes filename\": file containing the known classes " + + "(default: " + ); + sb.append(known_classes_filename); + + sb.append + ( + ").\n\"--groups filename\": file containing the groups (default: " + ); + sb.append(groups_filename); + + sb.append + ( + ").\n\"--threads number\": maximum number of threads running at " + + "any time (default: " + ); + sb.append(threads); + + sb.append + ( + ").\n\"--help\": prints this instead of converting (alias: \"-h\")." + ); + + System.out.println(sb.toString()); + } + + public static String get_output_directory () + { + return output_dir; + } + + public static String get_log_filename () + { + return log_filename; + } + + public static String get_known_classes_filename () + { + return known_classes_filename; + } + + public static String get_groups_filename () + { + return groups_filename; + } + + public static int get_max_threads () + { + return threads; + } +} |


