rcrdit/src/main/java/com/moparisthebest/rcrdit/RcrdIt.java

654 lines
28 KiB
Java

/*
* rcrdit records TV programs from TV tuners
* Copyright (C) 2017 Travis Burtrum
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.moparisthebest.rcrdit;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.moparisthebest.jdbc.QueryMapper;
import com.moparisthebest.rcrdit.autorec.AutoRec;
import com.moparisthebest.rcrdit.autorec.Profile;
import com.moparisthebest.rcrdit.autorec.ProgramAutoRec;
import com.moparisthebest.rcrdit.scheduler.PartialScheduler;
import com.moparisthebest.rcrdit.scheduler.Scheduler;
import com.moparisthebest.rcrdit.scheduler.StartStop;
import com.moparisthebest.rcrdit.tuner.DummyTuner;
import com.moparisthebest.rcrdit.tuner.HDHomerun;
import com.moparisthebest.rcrdit.tuner.Tuner;
import com.moparisthebest.rcrdit.tuner.Tuners;
import com.moparisthebest.rcrdit.xmltv.Channel;
import com.moparisthebest.rcrdit.xmltv.Program;
import com.moparisthebest.rcrdit.xmltv.Tv;
import jersey.repackaged.com.google.common.collect.Lists;
import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.TimeZoneRegistryFactory;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.property.*;
import org.glassfish.grizzly.http.server.CLStaticHttpHandler;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.StaticHttpHandler;
import org.glassfish.grizzly.http.server.StaticHttpHandlerBase;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;
import com.moparisthebest.sxf4j.impl.AbstractXmlElement;
import com.moparisthebest.sxf4j.impl.XmlElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.Consumes;
import javax.ws.rs.core.MediaType;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.*;
import java.time.*;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.Date;
import java.util.stream.Collectors;
import com.moparisthebest.rcrdit.requestbeans.GetScheduleRequest;
import com.moparisthebest.rcrdit.requestbeans.NewRecordingRequest;
import com.moparisthebest.rcrdit.responsebeans.GetScheduleResponse;
import org.glassfish.jersey.jackson.JacksonFeature;
/**
* Created by mopar on 2/16/17.
*/
@ApplicationPath("rest")
@Path("")
public class RcrdIt extends ResourceConfig implements AutoCloseable {
// for testing, should be null normally
private static final LocalDateTime fakeTime = null;//LocalDateTime.of(2017, 10, 29, 14, 26);
private static final Logger log = LoggerFactory.getLogger(RcrdIt.class);
// config items
private final Tuners tuners;
private final Set<String> allChannels;
private final List<String> xmltvPaths;
private final String databaseUrl;
private final TimerTask scheduleTask = new ScheduleTask();
private final Timer timer = new Timer();
private final List<TimerTask> startTimers = new ArrayList<>();
private final List<AutoRec> autoRecs = new ArrayList<>();
private Tv schedule;
private final Scheduler scheduler = new PartialScheduler();
private final String serverUri;
public RcrdIt(final File cfg) throws Exception {
register(this);
register(JacksonFeature.class);
final XmlElement rcrdit = AbstractXmlElement.getFactory().readFromFile(cfg);
this.databaseUrl = rcrdit.getChild("databaseUrl").getValue();
// this needs to end in /
String serverUri = rcrdit.getChild("serverUri").getValue();
if (!serverUri.endsWith("/"))
serverUri += "/";
this.serverUri = serverUri;
this.xmltvPaths = Arrays.stream(rcrdit.getChild("xmltvPaths").getChildren("xmltvPath")).map(XmlElement::getValue).collect(Collectors.toList());
this.allChannels = Arrays.stream(rcrdit.getChild("channels").getChildren("channel")).map(XmlElement::getValue).collect(Collectors.toSet());
final List<Tuner> tuners = new ArrayList<>();
for (final XmlElement tuner : rcrdit.getChild("tuners").getChildren("tuner")) {
switch (tuner.getAttribute("type")) {
case "HDHomeRun":
tuners.add(new HDHomerun(tuner.getAttribute("url")));
break;
case "Dummy":
tuners.add(new DummyTuner());
break;
default:
throw new IllegalArgumentException("unknown tuner type");
}
}
this.tuners = new Tuners(tuners);
//tuners.forEach(t -> allChannels.addAll(t.getChannels()));
//scheduleTask.run(); log.debug(this.toString()); System.exit(0);
// snatch autorecs
timer.schedule(new AutoRecTask(), 0);
// import schedule
timer.scheduleAtFixedRate(scheduleTask, 0, 12 * 60 * 60 * 1000); // every 12 hours?
/*
for(final Channel chan : schedule.getChannels())
for(final String name : chan.getDisplayNames())
if(name.length() < 5)
System.out.print("\"" + name + "\", ");
*/
}
private static void addMeeting(final Calendar calendar, final VTimeZone tz,
final Instant start, final Instant stop,
final Program prog, final MessageDigest md, final boolean skipped) {
addMeeting(calendar, tz, start, stop, new ProgramAutoRec(prog, prog.getAutoRec()), md, skipped);
}
private static void addMeeting(final Calendar calendar, final VTimeZone tz,
final Instant start, final Instant stop,
final ProgramAutoRec prog, final MessageDigest md, final boolean skipped) {
String name = prog.getProgram().getTitle();
String description = prog.getProgram().getDesc();
if (skipped) {
// since end and begin are the same we get alot of these for the same minute, ignore them
if (start.equals(stop))
return;
name = "SKIPPED: " + name;
}
// Create the event
final VEvent meeting = new VEvent(new DateTime(start.toEpochMilli()), new DateTime(stop.toEpochMilli()), name);
meeting.getProperties().add(new Location(prog.getProgram().getChannelName()));
final Duration oneMinute = Duration.of(1, ChronoUnit.MINUTES);
if (Duration.between(start, prog.getProgram().getStart()).abs().compareTo(oneMinute) > 0)
description += "\nMISSING BEGINNING";
if (Duration.between(stop, prog.getProgram().getStop()).abs().compareTo(oneMinute) > 0)
description += "\nMISSING ENDING";
description += "\nPriority: " + prog.getAutoRec().getPriority();
meeting.getProperties().add(new Description(description));
if(fakeTime != null)
meeting.getProperties().add(new DtStamp(new DateTime(1489294362645L)));
// add timezone info..
meeting.getProperties().add(tz.getTimeZoneId());
// generate unique identifier..
md.reset();
// incorporate everything to make this unique
md.update(Long.toString(start.toEpochMilli()).getBytes(UTF_8));
md.update(Long.toString(stop.toEpochMilli()).getBytes(UTF_8));
md.update(Long.toString(prog.getProgram().getStart().toEpochMilli()).getBytes(UTF_8));
md.update(Long.toString(prog.getProgram().getStop().toEpochMilli()).getBytes(UTF_8));
md.update(prog.getProgram().getChannelName().getBytes(UTF_8));
md.update(name.getBytes(UTF_8));
md.update(description.getBytes(UTF_8));
final byte[] digest = md.digest();
final Uid uid = new Uid(String.format("%0" + (digest.length << 1) + "x", new BigInteger(1, digest)));
/*
UidGenerator ug = null;
try {
ug = new UidGenerator("uidGen");
} catch (SocketException e) {
e.printStackTrace();
}
Uid uid = ug.generateUid();
*/
meeting.getProperties().add(uid);
if (skipped) {
// mark cancelled
meeting.getProperties().add(Status.VEVENT_CANCELLED);
}
calendar.getComponents().add(meeting);
}
private synchronized void reCalculateSchedule() {
log.debug("import starting.");
// cancel all pending timers, clear array, purge timer
startTimers.forEach(TimerTask::cancel);
startTimers.clear();
timer.purge();
schedule.getPrograms().forEach(Program::clear);
if (schedule.getPrograms().isEmpty() || autoRecs.isEmpty()) {
log.debug("nothing to import.");
return;
}
scheduler.schedulePrograms(autoRecs, schedule.getPrograms(), tuners.numTuners(), schedule.getLastEndTime(),
RcrdIt.fakeTime != null ?
RcrdIt.fakeTime.toInstant(ZoneOffset.systemDefault().getRules().getOffset(RcrdIt.fakeTime)).truncatedTo(ChronoUnit.MINUTES) :
Instant.now().truncatedTo(ChronoUnit.MINUTES)
);
final MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("no sha-256?", e);
}
// Create a TimeZone
final VTimeZone tz = TimeZoneRegistryFactory.getInstance().createRegistry().getTimeZone(ZoneId.systemDefault().getId()).getVTimeZone();
final Calendar calendar = new Calendar();
calendar.getProperties().add(new ProdId("-//rcrdit//rcrdit//EN"));
calendar.getProperties().add(Version.VERSION_2_0);
calendar.getProperties().add(CalScale.GREGORIAN);
// set up startTimers and rcrdit.ics
//for(final Program program : schedule.getPrograms()) {
//for(int x = 0; x < schedule.getPrograms().size(); ++x) {
for(int x = schedule.getPrograms().size() - 1; x >= 0; --x) {
final Program program = schedule.getPrograms().get(x);
if(program.getAutoRec() != null) {
// wanted to record
final List<StartStop> startStops = program.getStartStops();
if(startStops.isEmpty()) {
// whole thing skipped...
addMeeting(calendar, tz, program.getStart(), program.getStop(), program, md, true);
continue;
}
if((startStops.size() % 2) != 0) { // todo: this is really bad, wtf
System.out.println(program.fullString());
continue;
}
int index = -1;
final StartStop first = startStops.get(++index);
final StartStop second = startStops.get(++index);
if(startStops.size() == 2 && first.getInstant().equals(program.getStart()) && second.getInstant().equals(program.getStop())) {
// whole thing recorded...
Program toStop = first.getToStop();
addMeeting(calendar, tz, program.getStart(), program.getStop(), program, md, false);
RecordingTask rt = null;
if(toStop != null){
rt = new RecordingTask(toStop, program, program.getStart());
}else {
rt = new RecordingTask(program, program.getStart());
}
startTimers.add(rt);
continue;
}
if(first.isStart()) {
//Travis, uncomment me if I'm still needed
//if(first.getInstant().equals(program.getStart())) {
// not started at start time, skipped first part of program
//addMeeting(calendar, tz, program.getStart(), first.getInstant(), program, md, true);
//}
Program toStop = null;
Instant start = null, stop = null, lastStop = null;
for(final StartStop ss : program.getStartStops()) {
if(start == null && ss.isStart()) {
start = ss.getInstant();
toStop = ss.getToStop();
} else if(stop == null && !ss.isStart()) {
stop = ss.getInstant();
} else {
// both set
if(lastStop != null) { // todo: check if lastStop and start are the same, but shouldn't happen?
addMeeting(calendar, tz, lastStop, start, program, md, true);
}
addMeeting(calendar, tz, start, stop, program, md, false);
final RecordingTask rt = new RecordingTask(toStop, program, start);
startTimers.add(rt);
lastStop = stop;
start = stop = null;
}
}
if(lastStop != null && start != null) { // todo: check if lastStop and start are the same, but shouldn't happen?
addMeeting(calendar, tz, lastStop, start, program, md, true);
}
if(start != null && stop != null) {
addMeeting(calendar, tz, start, stop, program, md, false);
final RecordingTask rt = new RecordingTask(toStop, program, start);
startTimers.add(rt);
}
} else {
log.error("holy shit should never happen, bad scheduler?");
throw new RuntimeException("holy shit should never happen, bad scheduler?");
}
}
}
log.debug("new import done.\n\n------\n\n");
if(fakeTime != null) {
try (FileOutputStream fout = new FileOutputStream("startTimers.txt")) {
List<TimerTask> tmpTimers = Lists.reverse(startTimers);
for (TimerTask tt : tmpTimers) {
fout.write(tt.toString().getBytes(UTF_8));
fout.write('\n');
}
} catch (Exception e) {
// ignore e.printStackTrace();
}
try (FileOutputStream fout = new FileOutputStream("programs.txt")) {
for (Program prog : schedule.getPrograms()) {
fout.write(prog.fullString().getBytes(UTF_8));
fout.write('\n');
}
} catch (Exception e) {
// ignore e.printStackTrace();
}
}
if (!calendar.getComponents().isEmpty())
try (FileOutputStream fout = new FileOutputStream("rcrdit.ics")) {
final CalendarOutputter outputter = new CalendarOutputter();
outputter.output(calendar, fout);
} catch (Exception e) {
// ignore e.printStackTrace();
}
}
private static void removeStoppedShows(final VTimeZone tz, final Calendar calendar, final Map<ProgramAutoRec, Instant> recs, final Instant finalCursor, final MessageDigest md, final boolean skipped) {
//recs.keySet().removeIf(c -> c.getProgram().getStop().isBefore(finalCursor));
for (final Iterator<Map.Entry<ProgramAutoRec, Instant>> it = recs.entrySet().iterator(); it.hasNext(); ) {
final Map.Entry<ProgramAutoRec, Instant> entry = it.next();
final ProgramAutoRec par = entry.getKey();
final Program prog = par.getProgram();
if (!prog.getStop().isAfter(finalCursor)) {
//if(prog.getStop().isBefore(finalCursor)) {
final Instant start = entry.getValue();
addMeeting(calendar, tz, start, prog.getStop(), par, md, skipped);
if(!skipped)
prog.addStartStop(new StartStop(prog.getStop(), false));
it.remove();
}
}
}
@Override
public void close() {
timer.cancel();
// todo: check if all threads terminated gracefully? how?
tuners.close();
}
private class AutoRecTask extends TimerTask {
@Override
public void run() {
log.debug("Connecting database...");
try (Connection conn = DriverManager.getConnection(databaseUrl);
QueryMapper qm = new QueryMapper(conn)) {
log.debug("Database connected!");
final Map<Integer, Profile> profileMap = qm.toMap("SELECT profile_id, name, folder, run_at_recording_start, run_at_recording_finish FROM profile", new HashMap<>(), Integer.class, Profile.class);
//System.out.println(profileMap);
/*
try(java.sql.ResultSet rs = qm.toResultSet("SELECT profile_id, priority, title, channel_name, days_of_week AS daysOfWeekString, between_time_start AS betweenTimeStartTime, between_time_end AS betweenTimeEndTime, time_min AS timeMinDate, time_max AS timeMaxDate FROM autorecs")) {
for(int x = 1; x <= rs.getMetaData().getColumnCount(); ++x) {
System.out.println(rs.getMetaData().getColumnName(x));
System.out.println(rs.getMetaData().getColumnLabel(x));
}
}
*/
autoRecs.clear();
qm.toCollection("SELECT profile_id, priority, title, channel_name, days_of_week, between_time_start, between_time_end, time_min, time_max FROM autorecs", autoRecs, AutoRec.class);
autoRecs.forEach(a -> a.setProfile(profileMap.get(a.getProfileId())));
//autoRecs.add(new AutoRec(null, 1, "Judge Judy", null, null, null, null, null, null));
//autoRecs.add(new AutoRec(null, 5, "The Chew", null, null, null, null, null, null));
autoRecs.sort(AutoRec::compareTo);
//System.out.println(autoRecs);
// recalculate every time but first since schedule hasn't been fetched yet
if (schedule != null)
reCalculateSchedule();
} catch (SQLException e) {
e.printStackTrace();
//throw new IllegalStateException("Cannot connect the database!", e);
}
}
}
private class ScheduleTask extends TimerTask {
@Override
public void run() {
try {
schedule = Tv.readSchedule(xmltvPaths, allChannels, RcrdIt.fakeTime != null ? RcrdIt.fakeTime.withMinute(0) : LocalDateTime.now().withMinute(0));
//System.out.println(schedule);
// todo: only re-calcuate if schedule changed
reCalculateSchedule();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private class RecordingTask extends TimerTask {
private final ProgramAutoRec stop, start;
private final Instant runAt;
RecordingTask(final ProgramAutoRec stop, final ProgramAutoRec start, final Instant runAt) {
this.stop = stop;
this.start = start;
this.runAt = runAt;
if(fakeTime == null)
timer.schedule(this, Date.from(runAt));
}
RecordingTask(final ProgramAutoRec start, final Instant runAt) {
this(null, start, runAt);
}
RecordingTask(final Program stop, final Program start, final Instant runAt) {
this(stop == null ? null : new ProgramAutoRec(stop, stop.getAutoRec()), start == null ? null : new ProgramAutoRec(start, start.getAutoRec()), runAt);
}
RecordingTask(final Program start, final Instant runAt) {
this(null, start, runAt);
}
@Override
public void run() {
try {
if (this.stop == null)
tuners.recordNow(this.start, timer);
else
tuners.stopAndRecordNow(this.stop, this.start, timer);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "RecordingTask{" +
"stop=" + stop +
", start=" + start +
", runAt=" + runAt +
"} ";
}
}
@Override
public String toString() {
return "RcrdIt{" +
"tuners=" + tuners +
", allChannels=" + allChannels +
", databaseUrl='" + databaseUrl + '\'' +
", xmltvPaths='" + xmltvPaths + '\'' +
'}';
}
@GET
@Path("ping")
@Produces(MediaType.TEXT_PLAIN)
public String ping() {
return "pong";
}
@POST
@Path("getRecordingProfiles")
@Produces(MediaType.APPLICATION_JSON)
public Map<Integer, Profile> getRecordingProfiles() {
try (Connection conn = DriverManager.getConnection(databaseUrl);
QueryMapper qm = new QueryMapper(conn)) {
final Map<Integer, Profile> profileMap = qm.toMap("SELECT profile_id, name, folder, run_at_recording_start, run_at_recording_finish FROM profile", new HashMap<>(), Integer.class, Profile.class);
return profileMap;
}catch(Exception e){
log.error("Error in getSchedule",e);
}
return new HashMap<>();
}
@POST
@Path("getAutoRecs")
@Produces(MediaType.APPLICATION_JSON)
public List<AutoRec> getAutoRecs() {
return autoRecs;
}
@POST
@Path("getCurrentlyRecording")
@Produces(MediaType.APPLICATION_JSON)
public List<ProgramAutoRec> getCurrentlyRecording() {
List<ProgramAutoRec> currentlyRecording = new ArrayList<>();
tuners.getTuners().stream().map((tuner) -> tuner.getRecording()).filter((rec) -> (rec != null)).forEach((rec) -> {
currentlyRecording.add(rec);
});
return currentlyRecording;
}
@POST
@Path("getUpcomingRecordings")
@Produces(MediaType.APPLICATION_JSON)
public Map<Long,Program> getUpcomingRecordings() {
Map<Long,Program> returnMap = new LinkedHashMap<>();
for(TimerTask t : startTimers){
if(t instanceof RecordingTask){
RecordingTask rt = (RecordingTask)t;
if(rt.start != null && rt.scheduledExecutionTime() > System.currentTimeMillis()){
returnMap.put(rt.scheduledExecutionTime(),rt.start.getProgram());
}
}
}
return returnMap;
}
@POST
@Path("getSchedule")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public GetScheduleResponse getSchedule(GetScheduleRequest scheduleRequest) {
List<Channel> channelList = new ArrayList<>();
try{
if(scheduleRequest.getEndTime().isAfter(GetScheduleRequest.getLastPossibleDateToDisplayOnSchedule(schedule.getLastEndTime()))){
//not going to work, need to set things back a bit
scheduleRequest.setStartAndEndTimeBasedOnLastDataAvailable(schedule.getLastEndTime());
}
int firstItemToLoad = ((scheduleRequest.getPageNum()-1) * scheduleRequest.getChannelsPerPage());
if(schedule.getChannels().size() > firstItemToLoad){
for(int i=firstItemToLoad; i<schedule.getChannels().size() && channelList.size() <scheduleRequest.getChannelsPerPage() ;i++){
channelList.add(new Channel(schedule.getChannels().get(i),scheduleRequest.getStartTime(),scheduleRequest.getEndTime()));
}
}
}catch(Exception e){
log.error("Error in getSchedule",e);
}
return new GetScheduleResponse(scheduleRequest,channelList);
}
@POST
@Path("recordSingleInstanceOfProgram")
@Consumes(MediaType.APPLICATION_JSON)
public String recordSingleInstanceOfProgram(NewRecordingRequest recordingRequest) {
try (Connection conn = DriverManager.getConnection(databaseUrl);
QueryMapper qm = new QueryMapper(conn)) {
String sql = "INSERT INTO autorecs (autorec_id, profile_id, priority, title, channel_name, days_of_week, between_time_start, between_time_end, time_min, time_max) "
+ "VALUES (NULL, ?, ?, ?, ?, NULL, ?,?, from_unixtime(?), from_unixtime(?))";
Long startDate = null;
Long endDate = null;
if(recordingRequest.getStartDateEpochSeconds() != null)startDate = recordingRequest.getStartDateEpochSeconds();
if(recordingRequest.getEndDateEpochSeconds() != null)endDate = recordingRequest.getEndDateEpochSeconds();
String startTime = null;
String endTime = null;
if(recordingRequest.getStartTime() != null)startTime = recordingRequest.getStartTime().trim();
if(recordingRequest.getStopTime()!= null)endTime = recordingRequest.getStopTime().trim();
qm.executeUpdate(sql, recordingRequest.getProfileNo(),recordingRequest.getPriority(),recordingRequest.getTitle(),recordingRequest.getChannelName(),startTime,endTime,startDate,endDate);
timer.schedule(new AutoRecTask(), 0);
}catch(Exception e){
log.error("Error in recordSingleInstanceOfProgram",e);
}
return "OK";
}
@POST
@Path("refreshAutoRecs")
public void refreshAutoRecs() {
timer.schedule(new AutoRecTask(), 0);
}
public static void main(String[] args) throws Exception {
//System.out.println(System.currentTimeMillis()); if(true) return;
final File cfg;
if (args.length < 1 || !((cfg = new File(args[0])).exists())) {
System.err.println("Usage: java -jar rcrdit.jar /path/to/rcrdit.cfg.xml");
System.exit(1);
return;
}
log.debug("rcrdit starting");
final RcrdIt rcrdIt = new RcrdIt(cfg);
final ApplicationPath ap = RcrdIt.class.getAnnotation(ApplicationPath.class);
final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(URI.create(ap == null ? rcrdIt.serverUri : rcrdIt.serverUri + ap.value() + "/"), rcrdIt, false);
final File webapp = new File("./src/main/webapp/");
final StaticHttpHandlerBase staticHttpHandler;
if (new File(webapp, "index.html").canRead()) {
//staticHttpHandler = new CLStaticHttpHandler(new URLClassLoader(new URL[]{webapp.toURI().toURL()}));
staticHttpHandler = new StaticHttpHandler(webapp.getAbsolutePath());
staticHttpHandler.setFileCacheEnabled(false); // don't cache files, because we are in development?
System.out.println("File Caching disabled!");
} else {
staticHttpHandler = new CLStaticHttpHandler(RcrdIt.class.getClassLoader()); // jar class loader, leave cache enabled
}
server.getServerConfiguration().addHttpHandler(staticHttpHandler,
rcrdIt.serverUri.replaceFirst("^[^/]+//[^/]+", "")
);
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.debug("Shutdown requested");
server.shutdownNow();
rcrdIt.close();
log.debug("shutdown complete, exiting...");
System.exit(0);
}));
log.debug("rcrdit started");
}
}