/*
* Copyright 2011 Luke Usherwood.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.bettyluke.tracinstant.data;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import net.bettyluke.tracinstant.prefs.TracInstantProperties;
public class SiteData {
static final String TABULAR_CACHE_FILE = "SiteCache_Tabular.txt";
static final String HIDDEN_FIELDS_CACHE_FILE = "SiteCache_Hidden.txt";
private final TicketTableModel m_TableModel = new TicketTableModel();
private String dateTimeFormatString = null;
private DateTimeFormatter dateTimeFormat12 = null;
private DateTimeFormatter dateTimeFormat24 = null;
private String lastModifiedTicketTime;
private boolean hasConnected = false;
public SiteData() {
setDateFormat(TracInstantProperties.get().getValue("SiteDateFormat"));
}
public void saveState() {
SortedSet<String> userFields = m_TableModel.getUserFields();
saveTicketData(makeUserFieldsFileName(), userFields);
if (dateTimeFormatString != null) {
TracInstantProperties.get().putString("SiteDateFormat", dateTimeFormatString);
}
if (TracInstantProperties.getUseCache()) {
SortedSet<String> fields = new TreeSet<>(m_TableModel.getAllFields());
fields.removeAll(m_TableModel.getExcludedFields());
fields.removeAll(userFields);
saveTicketData(TABULAR_CACHE_FILE, fields);
fields = new TreeSet<>(m_TableModel.getExcludedFields());
fields.removeAll(userFields);
saveTicketData(HIDDEN_FIELDS_CACHE_FILE, fields);
}
}
public void loadUserData() {
try {
TicketProvider tp = loadTicketData(makeUserFieldsFileName());
if (tp != null) {
m_TableModel.mergeTicketsAsHidden(tp.getTickets());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public boolean isOkToUseCachedTickets() {
try {
return TracInstantProperties.getUseCache()
&& TracInstantProperties.getURL() != null
&& new File(getAppDataDir(), TABULAR_CACHE_FILE).canRead()
&& new File(getAppDataDir(), HIDDEN_FIELDS_CACHE_FILE).canRead();
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public TicketTableModel getTableModel() {
return m_TableModel;
}
// NB: Gets called from a worker thread as well as foreground thread.
// TODO: Separate loading/building out from data structures. Move into a TicketLoader
// class to sequence/coordinate loading all the various bits (see main TODO document).
static TicketProvider loadTicketData(String fileName) throws InterruptedException {
try {
File file = getAppFileForReading(fileName);
if (file != null) {
try (FileReader reader = new FileReader(file)) {
return TracTabTicketParser.parse(reader);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private static File getAppFileForReading(String simpleFileName) throws IOException {
File dataDir = getAppDataDir();
if (!dataDir.isDirectory()) {
return null;
}
File file = new File(dataDir, simpleFileName);
if (!file.exists()) {
return null;
}
return file;
}
private static File getAppDataDir() throws IOException {
return TracInstantProperties.get().getAppDataDirectory();
}
private void saveTicketData(String fileName, Set<String> fields) {
try {
File dataDir = getAppDataDir();
if (!dataDir.isDirectory() && !dataDir.mkdirs()) {
throw new IOException("Directory could not be created: " + dataDir);
}
File dataFile = new File(dataDir, fileName);
List<Ticket> tickets = m_TableModel.getTicketsWithAnyField(fields);
TabTicketWriter.write(new FileWriter(dataFile), fields, tickets);
} catch (IOException e) {
e.printStackTrace();
}
}
public void reset() {
deleteCachedDataFiles();
m_TableModel.clear();
lastModifiedTicketTime = null;
hasConnected = false;
}
private void deleteCachedDataFiles() {
deleteAppFile(TABULAR_CACHE_FILE);
deleteAppFile(HIDDEN_FIELDS_CACHE_FILE);
}
private void deleteAppFile(String name) {
try {
File file = new File(getAppDataDir(), name);
file.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
private static String makeUserFieldsFileName() {
String url = TracInstantProperties.getURL();
return extractAsciiCharacters(url) + "__LocalFields.txt";
}
private static String extractAsciiCharacters(String url) {
int colon = url.indexOf(':');
// We strip (e.g.) http, but LEAVE the colon ':'
if (colon >= 0 && colon <= 8) {
url = url.substring(colon);
}
String replaced = url.replaceAll("[^0-9a-zA-Z_]+", "-");
// Now strip the (original) ':' and the zero or more '/' characters, which have
// all been converted to a single '-'.
return replaced.substring(1);
}
public void setDateFormat(String dateFormat) {
dateTimeFormatString = dateFormat;
dateTimeFormat12 = dateFormat == null ? null :
newDateTimeFormatter(dateFormat, "[','][';'][' '][['t']h:m[:s][ ]a]");
dateTimeFormat24 = dateFormat == null ? null :
newDateTimeFormatter(dateFormat, "[','][';'][' '][['t']HH:mm[:ss]]");
}
private static DateTimeFormatter newDateTimeFormatter(String dateFormat, String timeFormat) {
return new DateTimeFormatterBuilder()
.parseLenient()
.parseCaseInsensitive()
.appendPattern(dateFormat)
.appendPattern(timeFormat)
.toFormatter();
}
public boolean isDateFormatSet() {
return dateTimeFormat12 != null;
}
public boolean hasConnected() {
return hasConnected;
}
/**
* @return null if no date-format is known, or if no (parseable) times were found.
*/
public String getLastModifiedTicketTimeIfKnown() {
return lastModifiedTicketTime;
}
public LocalDateTime parseDateTime(String str) throws DateTimeParseException {
try {
return dateTimeFormat12.parse(str, LocalDateTime::from);
} catch (DateTimeParseException ex) {
}
return dateTimeFormat24.parse(str, LocalDateTime::from);
}
public void setLastModifiedTicketTime(List<String> dateTimeStrings) {
hasConnected = true;
if (!isDateFormatSet()) {
return;
}
LocalDateTime latest = null;
String latestString = null;
for (String changeTime : dateTimeStrings) {
if (changeTime != null) {
try {
LocalDateTime date = parseDateTime(changeTime);
if (latest == null || date.isAfter(latest)) {
latest = date;
latestString = changeTime;
}
} catch (DateTimeParseException e) {
System.err.println("Date format not parseable: " + changeTime);
}
}
}
// HACK: not checking if greater, because in current usage it always will be
if (latestString != null) {
System.out.println("Last modified update: " + lastModifiedTicketTime + " -> " + latestString);
lastModifiedTicketTime = latestString;
}
}
public static void main(String[] args) {
TracInstantProperties.initialise("bettyluke.net", "TracInstantTests");
SiteData site = new SiteData();
site.setDateFormat("MMM dd, yyyy");
// Yeah, yeah, it's all yearning for unit tests. Problem with this particular project is
// I'm always just dipping in & in too much of a hurry to go speed up my life like that!
// TODO: Will do "Soon" ;-)
String[] dateStrings = new String[] {
"Jan 11, 2017, 1:30:37 PM",
"Jan 11, 2017",
"Jan 11, 2017 ",
"Jan 11, 2017 12", // Invalid
"Jan 11, 2017 12:", // Invalid
"Jan 11, 2017 12:58",
"Jan 11, 2017 12:58:", // Invalid
"Jan 11, 2017 12:58:38",
"Jan 11, 2017 12:58:38 ",
"Jan 11, 2017 2:58",
"Jan 11, 2017 2:58:", // Invalid
"Jan 11, 2017 2:58:38",
"Jan 11, 2017 20:58:38 ",
"Jan 11, 2017 12:58:38 PM",
"Jan 1, 2017, ",
"Jan 1, 2017, 12", // Invalid
"Jan 1, 2017, 12:", // Invalid
"1 1, 2017, 12:58",
"Jan 1, 17, 12:58:", // Invalid
"Jan 1, 17, 12:58:38",
"Jan 1, 17, 12:58:38 ",
"Jan 1, 17, 12:58:38 PM",
"Jan 1, 17, 1:8:", // Invalid
"Jan 1, 17, 1:8:8",
"Jan 1, 17, 20:08:08",
"Jan 1, 17, 20:8:8 PM",
"Jan 1, 17, 20:8:8 AM" // Invalid
};
for (String dateString : dateStrings) {
try {
System.out.print("date: " + dateString);
site.dateTimeFormat12.parse(dateString);
System.out.println(" parsed");
continue;
} catch (Exception e) {
System.out.println(" FAILED");
}
}
}
}