package no.java.incogito.application;
import fj.F;
import fj.F2;
import static fj.Function.compose;
import fj.P1;
import fj.P2;
import fj.P;
import static fj.P.p;
import fj.control.parallel.Callables;
import fj.data.List;
import static fj.data.List.list;
import static fj.data.List.nil;
import fj.data.Option;
import static fj.data.Option.some;
import static fj.data.Option.none;
import static fj.data.Option.somes;
import fj.data.TreeMap;
import fj.data.Either;
import fj.pre.Ord;
import no.java.incogito.Functions;
import no.java.incogito.IO;
import static no.java.incogito.IO.Strings.streamToString;
import no.java.incogito.PropertiesF;
import static no.java.incogito.Functions.trim;
import static no.java.incogito.Functions.split;
import no.java.incogito.application.IncogitoConfiguration.EventConfiguration;
import no.java.incogito.application.IncogitoConfiguration.DayConfiguration;
import no.java.incogito.domain.CssConfiguration;
import static no.java.incogito.domain.Event.emptyLevelIconMap;
import no.java.incogito.domain.Label;
import no.java.incogito.domain.Level;
import no.java.incogito.domain.Room;
import no.java.incogito.domain.Level.LevelId;
import no.java.incogito.ems.client.EmsWrapper;
import no.java.ems.domain.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.joda.time.LocalDate;
import org.joda.time.Interval;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import java.io.File;
/**
* TODO: This should take an existing configuration as an argument and use that if none of the
* resources on disk have changed.
*
* @author <a href="mailto:trygve.laugstol@arktekk.no">Trygve Laugstøl</a>
* @version $Id$
*/
@Component
public class ConfigurationLoaderService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final EmsWrapper emsWrapper;
DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().
appendYear(4, 4).
appendLiteral('-').
appendMonthOfYear(2).
appendLiteral('-').
appendDayOfMonth(2).
toFormatter();
DateTimeFormatter timeslotFormatter = new DateTimeFormatterBuilder().
appendHourOfDay(2).
appendMinuteOfHour(2).
toFormatter();
@Autowired
public ConfigurationLoaderService(EmsWrapper emsWrapper) {
this.emsWrapper = emsWrapper;
}
public IncogitoConfiguration loadConfiguration(File incogitoHome, IncogitoConfiguration existingConfiguration) throws Exception {
File props = new File(incogitoHome, "etc/incogito.properties").getAbsoluteFile();
// logger.info("Reloading configuration from: " + props);
File etc = props.getParentFile();
TreeMap<String, String> properties = IO.<TreeMap<String, String>>runFileInputStream_().
f(PropertiesF.loadPropertiesAsMap).
f(props).call();
String baseurl = properties.get("baseurl").toEither("").right().valueE(p("Missing required property: 'baseurl'"));
String eventsConfiguration = properties.get("events").toEither("").right().valueE(p("Missing required property: 'events'"));
F<String, Option<Double>> parseDouble = compose(Functions.<NumberFormatException, Double>Either_rightToOption_(), Functions.parseDouble);
double sessionEmStart = properties.get("sessionEmStart").bind(parseDouble).orSome(CssConfiguration.defaultCssConfiguration.sessionEmStart);
double emPerMinute = properties.get("emPerMinute").bind(parseDouble).orSome(CssConfiguration.defaultCssConfiguration.emPerMinute);
double emPerRoom = properties.get("emPerRoom").bind(parseDouble).orSome(CssConfiguration.defaultCssConfiguration.emPerRoom);
CssConfiguration cssConfiguration = new CssConfiguration(sessionEmStart, emPerMinute, emPerRoom);
File eventsDirectory = new File(etc, "events");
Option<String> frontPageContent = some(new File(etc, "frontpage.txt")).
filter(Functions.File_canRead).
map(IO.<String>runFileInputStream_().f(streamToString)).
bind(compose(P1.<Option<String>>__1(), Callables.<String>option()));
Option<String> aboutContent = some(new File(etc, "about.txt")).
filter(Functions.File_canRead).
map(IO.<String>runFileInputStream_().f(streamToString)).
bind(compose(P1.<Option<String>>__1(), Callables.<String>option()));
List<EventConfiguration> events = nil();
// TODO: Check for icons on disk and put those in a map, including alternative texts for each image
// TODO: Load the order of the rooms
// TODO: Load "extra" room sessions which are special like "lunch" and "party zone"
// TODO: Consider switching to reading a <event id>.xml file if it exist and use that as configuration
for (final String eventName : list(eventsConfiguration.split(",")).map(trim)) {
final File eventDirectory = new File(eventsDirectory, eventName);
if (!eventDirectory.isDirectory()) {
logger.warn("Missing configuration for event '" + eventName + "'.");
}
File eventPropertiesFile = new File(eventDirectory, "event.properties");
Option<EventConfiguration> existingEventConfiguration = existingConfiguration.findEventConfigurationByName(eventName);
if(existingEventConfiguration.isSome() && !existingEventConfiguration.some().isOutdated(eventPropertiesFile.lastModified())) {
events = events.cons(existingEventConfiguration.some());
continue;
}
logger.warn("Reloading configuration for event '" + eventName + "'");
final TreeMap<String, String> eventProperties = Callables.option(IO.<TreeMap<String, String>>runFileInputStream_().
f(PropertiesF.loadPropertiesAsMap).
f(eventPropertiesFile))._1().orSome(TreeMap.<String, String>empty(Ord.stringOrd));
Option<String> blurb = eventProperties.get("blurb");
List<Label> labels = Option.somes(List.list(eventProperties.get("labels").orSome("").split(",")).
map(trim).
map(new F<String, Option<Label>>() {
public Option<Label> f(String emsId) {
// If the configuration file contain an ".id" element, use that as the internal id
String id = eventProperties.get(emsId + ".id").orSome(emsId);
if (id.indexOf(' ') != -1) {
logger.warn("Invalid id for ems label '" + emsId + "'. Override the id by adding a '" + emsId + ".id' property to the event configuration.");
return none();
}
File iconFile = new File(eventDirectory, "labels/" + id + ".png");
Option<Label> label = some(id).bind(some(emsId), eventProperties.get(emsId + ".displayName"), Option.iif(Functions.File_canRead, iconFile), Label.label_);
if (!label.isSome()) {
logger.warn("Could not find file for label: " + id);
}
return label;
}
}));
TreeMap<LevelId, Level> levelMap = List.list(eventProperties.get("levels").orSome("").split(",")).
map(trim).
foldLeft(new F2<TreeMap<LevelId, Level>, String, TreeMap<LevelId, Level>>() {
public TreeMap<LevelId, Level> f(TreeMap<LevelId, Level> levelIcons, String id) {
File iconFile = new File(eventDirectory, "levels/" + id + ".png");
Option<Level> level = LevelId.valueOf_.f(id).bind(eventProperties.get(id + ".displayName"), Option.iif(Functions.File_canRead, iconFile), Level.level_);
if (level.isSome()) {
return levelIcons.set(level.some().id, level.some());
}
return levelIcons;
}
}, emptyLevelIconMap);
Either<String, Event> eventEither = emsWrapper.findEventByName.f(eventName);
if (eventEither.isLeft()) {
logger.warn("Could not find event '{}' in EMS: ", eventName, eventEither.left().value());
continue;
}
List<Room> presentationRooms = eventProperties.get("room.presentation").
map(split.f(",")).orSome(List.<String>nil()).
map(compose(Room.room_, trim));
List<P2<LocalDate, DayConfiguration>> days = eventProperties.get("dates").
map(split.f(",")).orSome(List.<String>nil()).
map(new F<String, LocalDate>() {
public LocalDate f(String s) {
return dateFormatter.parseDateTime(s.trim()).toLocalDate();
}
}).
map(new F<LocalDate, P2<LocalDate, DayConfiguration>>() {
public P2<LocalDate, DayConfiguration> f(LocalDate localDate) {
String roomsKey = dateFormatter.print(localDate) + ".rooms";
Option<String> roomsOption = eventProperties.get(roomsKey);
String timeslotsKey = dateFormatter.print(localDate) + ".timeslots";
Option<String> timeslotsOption = eventProperties.get(timeslotsKey);
List<Room> rooms = nil();
List<Interval> timeslots = nil();
if(roomsOption.isNone()) {
logger.warn("Missing room configuration: " + roomsKey);
return P.p(localDate, new DayConfiguration(rooms, timeslots));
}
if(timeslotsOption.isNone()) {
logger.warn("Missing timeslots configuration: " + timeslotsKey);
return P.p(localDate, new DayConfiguration(rooms, timeslots));
}
rooms = roomsOption.map(split.f(",")).orSome(List.<String>nil()).
map(compose(Room.room_, trim));
final DateTime dateTime = localDate.toDateMidnight().toDateTime();
timeslots = somes(timeslotsOption.map(split.f(",")).orSome(List.<String>nil()).
map(trim).
map(new F<String, Option<Interval>>() {
public Option<Interval> f(String s) {
List<String> parts = split.f("-").f(s);
if(parts.length() != 2) {
logger.warn("Invalid timeslot: " + s);
return none();
}
DateTime start = timeslotFormatter.parseDateTime(parts.index(0));
DateTime end = timeslotFormatter.parseDateTime(parts.index(1));
return some(new Interval(
dateTime.withHourOfDay(start.getHourOfDay()).withMinuteOfHour(start.getMinuteOfHour()),
dateTime.withHourOfDay(end.getHourOfDay()).withMinuteOfHour(end.getMinuteOfHour())));
}
}));
return P.p(localDate, new DayConfiguration(rooms, timeslots));
}
});
if(days.isEmpty()) {
logger.warn("Misconfiguration: missing a 'dates' property.");
continue;
}
events = events.cons(new EventConfiguration(eventName, blurb, days,
presentationRooms, labels, levelMap, eventPropertiesFile.lastModified()));
}
return new IncogitoConfiguration(baseurl, cssConfiguration, frontPageContent, aboutContent, events.reverse());
}
}