diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..b3d4bf5
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/annotations-java8.jar b/lib/annotations-java8.jar
new file mode 100644
index 0000000..857c151
Binary files /dev/null and b/lib/annotations-java8.jar differ
diff --git a/lib/guava-21.0.jar b/lib/guava-21.0.jar
new file mode 100644
index 0000000..0618195
Binary files /dev/null and b/lib/guava-21.0.jar differ
diff --git a/lib/json.jar b/lib/json.jar
new file mode 100644
index 0000000..0e6bd4d
Binary files /dev/null and b/lib/json.jar differ
diff --git a/lib/org.everit.json.schema-1.5.0.jar b/lib/org.everit.json.schema-1.5.0.jar
new file mode 100644
index 0000000..1ca2ea6
Binary files /dev/null and b/lib/org.everit.json.schema-1.5.0.jar differ
diff --git a/resources/data/sources_v1.0.json b/resources/data/sources_v1.0.json
new file mode 100644
index 0000000..04e245f
--- /dev/null
+++ b/resources/data/sources_v1.0.json
@@ -0,0 +1,52 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "DataSources",
+ "description": "Helioviewer DataSources API response",
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "oneOf": [
+ { "$ref": "#/definitions/tree_node" },
+ { "$ref": "#/definitions/tree_leaf" }
+ ]
+ }
+ },
+ "definitions": {
+ "root_part": {
+ "patternProperties": {
+ "^.*$": {
+ "oneOf": [
+ { "$ref": "#/definitions/tree_node" },
+ { "$ref": "#/definitions/tree_leaf" }
+ ]
+ }
+ }
+ },
+ "tree_node": {
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "children": { "$ref": "#/definitions/root_part" },
+ "default": { "type" : "boolean" }
+ },
+ "required": [ "name", "description", "children" ]
+ },
+ "tree_leaf": {
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "sourceId": { "type": "integer" },
+ "start": {
+ "type": "string",
+ "format": "sql-date-time"
+ },
+ "end": {
+ "type": "string",
+ "format": "sql-date-time"
+ },
+ "default": { "type" : "boolean" }
+ },
+ "required": [ "name", "description", "sourceId", "start", "end" ]
+ }
+ }
+}
diff --git a/src/org/helioviewer/jhv/DataSourcesChecker.java b/src/org/helioviewer/jhv/DataSourcesChecker.java
new file mode 100644
index 0000000..c0ce58c
--- /dev/null
+++ b/src/org/helioviewer/jhv/DataSourcesChecker.java
@@ -0,0 +1,11 @@
+package org.helioviewer.jhv;
+
+import org.helioviewer.jhv.io.DataSources;
+
+public class DataSourcesChecker {
+
+ public static void main(String[] args) {
+ DataSources.loadSources();
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/JHVGlobals.java b/src/org/helioviewer/jhv/JHVGlobals.java
new file mode 100644
index 0000000..b2bb570
--- /dev/null
+++ b/src/org/helioviewer/jhv/JHVGlobals.java
@@ -0,0 +1,25 @@
+package org.helioviewer.jhv;
+
+import java.util.concurrent.ExecutorService;
+
+import org.helioviewer.jhv.threads.JHVExecutor;
+
+public class JHVGlobals {
+
+ public static String userAgent = "DataSources-schema";
+
+ private static final ExecutorService executorService = JHVExecutor.getJHVWorkersExecutorService("MAIN", 10);
+
+ public static ExecutorService getExecutorService() {
+ return executorService;
+ }
+
+ public static int getStdReadTimeout() {
+ return 180000;
+ }
+
+ public static int getStdConnectTimeout() {
+ return 60000;
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/base/DownloadStream.java b/src/org/helioviewer/jhv/base/DownloadStream.java
new file mode 100644
index 0000000..d2bf70b
--- /dev/null
+++ b/src/org/helioviewer/jhv/base/DownloadStream.java
@@ -0,0 +1,206 @@
+package org.helioviewer.jhv.base;
+
+import java.awt.EventQueue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.InflaterInputStream;
+
+import org.helioviewer.jhv.JHVGlobals;
+import org.helioviewer.jhv.base.logging.Log;
+
+public class DownloadStream {
+
+ // Input stream to read the data from
+ private InputStream in = null;
+
+ // Output to send as a post request
+ private String output = null;
+
+ // Suggested name to save (if wanted)
+ private String outputName = null;
+
+ private String contentDisposition = null;
+ private int contentLength = -1;
+ private boolean response400 = false;
+
+ // Read timeout in ms
+ private final int readTimeout;
+
+ // Connect timeout in ms
+ private final int connectTimeout;
+
+ // URL to connect
+ private final URL url;
+ private final boolean ignore400;
+
+ private DownloadStream(URL _url, int _connectTimeout, int _readTimeout, boolean _ignore400) {
+ url = _url;
+ readTimeout = _readTimeout;
+ connectTimeout = _connectTimeout;
+ ignore400 = _ignore400;
+ }
+
+ private DownloadStream(URL _url, boolean _ignore400) {
+ this(_url, JHVGlobals.getStdConnectTimeout(), JHVGlobals.getStdReadTimeout(), _ignore400);
+ }
+
+ public DownloadStream(String _url, boolean _ignore400) throws MalformedURLException {
+ this(new URL(_url), _ignore400);
+ }
+
+ public DownloadStream(URL _url) {
+ this(_url, JHVGlobals.getStdConnectTimeout(), JHVGlobals.getStdReadTimeout(), false);
+ }
+
+ public DownloadStream(String _url) throws MalformedURLException {
+ this(new URL(_url));
+ }
+
+ public boolean isResponse400() {
+ return response400;
+ }
+
+ private static InputStream getEncodedStream(String encoding, InputStream httpStream) throws IOException {
+ if (encoding != null) {
+ switch (encoding.toLowerCase()) {
+ case "gzip":
+ return new GZIPInputStream(httpStream);
+ case "deflate":
+ return new InflaterInputStream(httpStream);
+ }
+ }
+ return httpStream;
+ }
+
+ /**
+ * Opens the connection with compression if the server supports
+ *
+ * @throws IOException
+ * From accessing the network
+ */
+ private void connect() throws IOException {
+ if (EventQueue.isDispatchThread())
+ throw new IOException("Don't do that");
+
+ //Log.debug("Connect to " + url);
+ URLConnection connection = url.openConnection();
+ // Set timeouts
+ connection.setConnectTimeout(connectTimeout);
+ connection.setReadTimeout(readTimeout);
+
+ if (connection instanceof HttpURLConnection) {
+ HttpURLConnection httpC = (HttpURLConnection) connection;
+ // get compression if supported
+ httpC.setRequestProperty("Accept-Encoding", "gzip,deflate");
+ httpC.setRequestProperty("User-Agent", JHVGlobals.userAgent);
+
+ // Write post data if necessary
+ if (output != null) {
+ connection.setDoOutput(true);
+ try (OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8)) {
+ out.write(output);
+ }
+ }
+
+ try {
+ httpC.connect();
+ } catch (IOException e) {
+ Log.warn("HTTP connection failed: " + url + " " + e);
+ }
+
+ // Check the connection code
+ int code = httpC.getResponseCode();
+ if (code > 400) {
+ throw new IOException("Error opening HTTP connection to " + url + " Response code: " + code);
+ }
+
+ if (!ignore400 && code == 400) {
+ throw new IOException("Error opening HTTP connection to " + url + " Response code: " + code);
+ }
+
+ InputStream strm;
+ if (code == 400) {
+ response400 = true;
+ strm = httpC.getErrorStream();
+ if (strm == null)
+ strm = httpC.getInputStream();
+ } else {
+ strm = httpC.getInputStream();
+ }
+
+ contentDisposition = httpC.getHeaderField("Content-Disposition");
+ in = getEncodedStream(httpC.getContentEncoding(), strm);
+ } else {
+ // Not an http connection
+ // Write post data if necessary
+ if (output != null) {
+ connection.setDoOutput(true);
+ try (OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8)) {
+ out.write(output);
+ }
+ }
+ // Okay just normal
+ in = connection.getInputStream();
+ }
+ contentLength = connection.getContentLength();
+ }
+
+ /**
+ * Gives the outstream to read the response, after calling connect. If it is
+ * not already connected it will automatically connect
+ *
+ * @return output stream of the connection
+ * @throws IOException
+ * Error from creating the connction
+ */
+ public InputStream getInput() throws IOException {
+ if (in == null)
+ connect();
+ return in;
+ }
+
+ /**
+ * After requesting the data the associated file name to save from
+ * Content-Disposition or the url name
+ *
+ * @return suggested download name
+ */
+ public String getOutputName() {
+ if (outputName == null) {
+ if (contentDisposition != null) {
+ Matcher m = Regex.ContentDispositionFilename.matcher(contentDisposition);
+ if (m.find()) {
+ outputName = m.group(1);
+ }
+ }
+ if (outputName == null) {
+ outputName = url.getFile().replace('/', '-');
+ }
+ }
+ return outputName;
+ }
+
+ public int getContentLength() {
+ return contentLength;
+ }
+
+ /**
+ * Set the output to send to the server (in HTTP as POST)
+ *
+ * @param _output
+ * Send output to the server, null if nothing (GET in HTTP)
+ */
+ public void setOutput(String _output) {
+ output = _output;
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/base/FileUtils.java b/src/org/helioviewer/jhv/base/FileUtils.java
new file mode 100644
index 0000000..6ab64db
--- /dev/null
+++ b/src/org/helioviewer/jhv/base/FileUtils.java
@@ -0,0 +1,130 @@
+package org.helioviewer.jhv.base;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Scanner;
+
+// A class which provides functions for accessing and working with files
+public class FileUtils {
+
+ private static final int BUFSIZ = 65536;
+
+ /**
+ * Return the current working directory
+ *
+ * @return the current working directory
+ */
+ public static File getWorkingDirectory() {
+ return new File(System.getProperty("user.dir"));
+ }
+
+ /**
+ * Method copies a file from src to dst.
+ *
+ * @param src
+ * Source file
+ * @param dst
+ * Destination file
+ * @throws IOException
+ */
+ public static void copy(File src, File dst) throws IOException {
+ try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) {
+ // Transfer bytes from in to out
+ byte[] buf = new byte[BUFSIZ];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ }
+ }
+
+ /**
+ * Method saving a stream to dst.
+ *
+ * @param in
+ * Input stream, will be closed if finished
+ * @param dst
+ * Destination file
+ * @throws IOException
+ */
+ public static void save(InputStream in, File dst) throws IOException {
+ // Transfer bytes from in to out
+ try (OutputStream out = new FileOutputStream(dst)) {
+ byte[] buf = new byte[BUFSIZ];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ }
+ in.close();
+ }
+
+ /**
+ * Returns an input stream to a resource. This function can be used even if
+ * the whole program and resources are within a JAR file. The path must
+ * begin with a slash and contain all subfolders, e.g.: /images/sample_image.png
+ * The class loader used is the same which was used to load FileUtils.
+ *
+ * @param resourcePath
+ * The path to the resource
+ * @return An InputStream to the resource
+ */
+ public static InputStream getResourceInputStream(String resourcePath) {
+ return FileUtils.class.getResourceAsStream(resourcePath);
+ }
+
+ public static String convertStreamToString(InputStream is) {
+ try (Scanner s = new Scanner(is, StandardCharsets.UTF_8.name())) {
+ s.useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
+ }
+
+ /**
+ * Returns an URL to a resource. This function can be used even if the whole
+ * program and resources are within a JAR file. The path must begin with a
+ * slash and contain all subfolders, e.g.: /images/sample_image.png
+ * The class loader used is the same which was used to load FileUtils.
+ *
+ * @param resourcePath
+ * The path to the resource
+ * @return An URL to the resource
+ */
+ public static URL getResourceUrl(String resourcePath) {
+ return FileUtils.class.getResource(resourcePath);
+ }
+
+ public static boolean deleteDir(File dir) {
+ String[] children;
+ if (dir.isDirectory() && (children = dir.list()) != null) {
+ for (String child : children) {
+ boolean success = deleteDir(new File(dir, child));
+ if (!success) {
+ return false;
+ }
+ }
+ }
+ return dir.delete();
+ }
+
+ public static String URL2String(URL url) {
+ StringBuilder sb = new StringBuilder();
+ try (Scanner scanner = new Scanner(new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)))) {
+ while (scanner.hasNext()) {
+ sb.append(scanner.nextLine()).append('\n');
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/base/JSONUtils.java b/src/org/helioviewer/jhv/base/JSONUtils.java
new file mode 100644
index 0000000..80b8230
--- /dev/null
+++ b/src/org/helioviewer/jhv/base/JSONUtils.java
@@ -0,0 +1,38 @@
+package org.helioviewer.jhv.base;
+
+import java.io.ByteArrayOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.GZIPOutputStream;
+
+import org.helioviewer.jhv.base.logging.Log;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+public class JSONUtils {
+
+ private static final int BUFSIZ = 65536;
+
+ public static JSONObject getJSONStream(InputStream in) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8), BUFSIZ)) {
+ return new JSONObject(new JSONTokener(reader));
+ } catch (Exception e) {
+ Log.error("Invalid JSON response " + e);
+ return new JSONObject();
+ }
+ }
+
+ public static byte[] compressJSON(JSONObject json) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ OutputStreamWriter out = new OutputStreamWriter(new GZIPOutputStream(baos, BUFSIZ), StandardCharsets.UTF_8);
+ json.write(out);
+ out.close();
+
+ return baos.toByteArray();
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/base/Regex.java b/src/org/helioviewer/jhv/base/Regex.java
new file mode 100644
index 0000000..6ed9cd7
--- /dev/null
+++ b/src/org/helioviewer/jhv/base/Regex.java
@@ -0,0 +1,76 @@
+package org.helioviewer.jhv.base;
+
+import java.util.regex.Pattern;
+
+public class Regex {
+
+ // https://github.com/android/platform_frameworks_base/blob/master/core/java/android/util/Patterns.java
+
+ /**
+ * Good characters for Internationalized Resource Identifiers (IRI).
+ * This comprises most common used Unicode characters allowed in IRI
+ * as detailed in RFC 3987.
+ * Specifically, those two byte Unicode characters are not included.
+ */
+ private static final String GOOD_IRI_CHAR =
+ "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
+
+ private static final Pattern IP_ADDRESS
+ = Pattern.compile(
+ "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ + "|[1-9][0-9]|[0-9]))");
+
+ /**
+ * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
+ */
+ private static final String IRI
+ = "[" + GOOD_IRI_CHAR + "]([" + GOOD_IRI_CHAR + "\\-]{0,61}[" + GOOD_IRI_CHAR + "]){0,1}";
+
+ private static final String GOOD_GTLD_CHAR =
+ "a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
+ private static final String GTLD = "[" + GOOD_GTLD_CHAR + "]{2,63}";
+ private static final String HOST_NAME = "(" + IRI + "\\.)+" + GTLD;
+
+ private static final Pattern DOMAIN_NAME
+ = Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")");
+
+ /**
+ * Regular expression pattern to match most part of RFC 3987
+ * Internationalized URLs, aka IRIs. Commonly used Unicode characters are
+ * added.
+ */
+ public static final Pattern WEB_URL = Pattern.compile(
+ "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?"
+ + "(?:" + DOMAIN_NAME + ")"
+ + "(?:\\:\\d{1,5})?)" // plus option port number
+ + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params
+ + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?"
+ + "(?:\\b|$)"); // and finally, a word boundary or end of
+ // input. This is to stop foo.sure from
+ // matching as foo.su
+
+ public static final Pattern EMAIL_ADDRESS
+ = Pattern.compile(
+ "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
+ "\\@" +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
+ "(" +
+ "\\." +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
+ ")+"
+ );
+
+
+ public static final Pattern HREF = Pattern.compile("href=\"(.*?)\"");
+
+ public static final Pattern FloatingPoint = Pattern.compile("[\\x00-\\x20]*[+-]?(NaN|Infinity|((((\\p{Digit}+)(\\.)?((\\p{Digit}+)?)([eE][+-]?(\\p{Digit}+))?)|(\\.((\\p{Digit}+))([eE][+-]?(\\p{Digit}+))?)|(((0[xX](\\p{XDigit}+)(\\.)?)|(0[xX](\\p{XDigit}+)?(\\.)(\\p{XDigit}+)))[pP][+-]?(\\p{Digit}+)))[fFdD]?))[\\x00-\\x20]*");
+ public static final Pattern Integer = Pattern.compile("\\d+");
+
+ // Pattern to extract the filename from HTTP Content-Disposition header
+ public static final Pattern ContentDispositionFilename = Pattern.compile("filename=\\\"(.*?)\\\"");
+
+}
diff --git a/src/org/helioviewer/jhv/base/logging/Log.java b/src/org/helioviewer/jhv/base/logging/Log.java
new file mode 100644
index 0000000..93e2efa
--- /dev/null
+++ b/src/org/helioviewer/jhv/base/logging/Log.java
@@ -0,0 +1,21 @@
+package org.helioviewer.jhv.base.logging;
+
+public class Log {
+
+ public static void warn(Object obj) {
+ System.err.println(obj.toString());
+ }
+
+ public static void error(Object obj) {
+ System.err.println(obj.toString());
+ }
+
+ public static void error(Object obj, Throwable error) {
+ System.err.println(obj.toString() + error.toString());
+ }
+
+ public static void debug(Object obj) {
+ System.out.println(obj.toString());
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/base/time/JHVDate.java b/src/org/helioviewer/jhv/base/time/JHVDate.java
new file mode 100644
index 0000000..6fc1eca
--- /dev/null
+++ b/src/org/helioviewer/jhv/base/time/JHVDate.java
@@ -0,0 +1,44 @@
+package org.helioviewer.jhv.base.time;
+
+import org.jetbrains.annotations.NotNull;
+
+public class JHVDate implements Comparable {
+
+ private final String string;
+ public final long milli;
+
+ public JHVDate(String date) {
+ this(TimeUtils.parse(date));
+ }
+
+ public JHVDate(long _milli) {
+ if (_milli < 0)
+ throw new IllegalArgumentException("Argument cannot be negative");
+ milli = _milli;
+ string = TimeUtils.format(milli);
+ }
+
+ @Override
+ public int compareTo(@NotNull JHVDate dt) {
+ return milli < dt.milli ? -1 : (milli > dt.milli ? +1 : 0);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof JHVDate))
+ return false;
+ JHVDate d = (JHVDate) o;
+ return milli == d.milli;
+ }
+
+ @Override
+ public int hashCode() {
+ return (int) (milli ^ (milli >>> 32));
+ }
+
+ @Override
+ public String toString() {
+ return string;
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/base/time/TimeUtils.java b/src/org/helioviewer/jhv/base/time/TimeUtils.java
new file mode 100644
index 0000000..5be6193
--- /dev/null
+++ b/src/org/helioviewer/jhv/base/time/TimeUtils.java
@@ -0,0 +1,91 @@
+package org.helioviewer.jhv.base.time;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Optional;
+
+import org.everit.json.schema.FormatValidator;
+
+public class TimeUtils {
+
+ private static final ZoneOffset ZERO = ZoneOffset.ofTotalSeconds(0);
+ private static final DateTimeFormatter sqlFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+ private static final DateTimeFormatter fileFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
+
+ public static final long DAY_IN_MILLIS = 86400000;
+ public static final long MINUTE_IN_MILLIS = 60000;
+
+ public static final JHVDate EPOCH = new JHVDate("2000-01-01T00:00:00");
+ public static final JHVDate MINIMAL_DATE = new JHVDate("1970-01-01T00:00:00");
+ public static final JHVDate MAXIMAL_DATE = new JHVDate("2050-01-01T00:00:00");
+
+ public static String format(long milli) {
+ return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(Instant.ofEpochMilli(milli).atOffset(ZERO));
+ }
+
+ public static String formatZ(long milli) {
+ return DateTimeFormatter.ISO_INSTANT.format(Instant.ofEpochMilli(milli));
+ }
+
+ public static String formatSQL(long milli) {
+ return sqlFormatter.format(Instant.ofEpochMilli(milli).atOffset(ZERO));
+ }
+
+ public static String formatDate(long milli) {
+ return DateTimeFormatter.ISO_LOCAL_DATE.format(Instant.ofEpochMilli(milli).atOffset(ZERO));
+ }
+
+ public static String formatTime(long milli) {
+ return DateTimeFormatter.ISO_LOCAL_TIME.format(Instant.ofEpochMilli(milli).atOffset(ZERO));
+ }
+
+ public static String formatFilename(long milli) {
+ return fileFormatter.format(Instant.ofEpochMilli(milli).atOffset(ZERO));
+ }
+
+ public static long parse(String date) {
+ return LocalDateTime.parse(date, DateTimeFormatter.ISO_LOCAL_DATE_TIME).toInstant(ZERO).toEpochMilli();
+ }
+
+ public static long parseSQL(String date) {
+ return LocalDateTime.parse(date, sqlFormatter).toInstant(ZERO).toEpochMilli();
+ }
+
+ public static long parseDate(String date) {
+ return LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE).toEpochDay() * DAY_IN_MILLIS;
+ }
+
+ public static long parseTime(String date) {
+ return LocalTime.parse(date, DateTimeFormatter.ISO_LOCAL_TIME).toSecondOfDay() * 1000L;
+ }
+
+ public static class SQLDateTimeFormatValidator implements FormatValidator {
+
+ @Override
+ public Optional validate(final String subject) {
+ try {
+ long time = parseSQL(subject);
+ if (time < MINIMAL_DATE.milli || time > MAXIMAL_DATE.milli)
+ throw new Exception();
+
+ return Optional.empty();
+ } catch (DateTimeParseException e) {
+ return Optional.of(String.format("[%s] is not a valid sql-date-time.", subject));
+ } catch (Exception e) {
+ return Optional.of(String.format("[%s] is outside date range of [%s,%s].", subject, MINIMAL_DATE, MAXIMAL_DATE));
+ }
+ }
+
+ @Override
+ public String formatName() {
+ return "sql-date-time";
+ }
+
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/io/DataSources.java b/src/org/helioviewer/jhv/io/DataSources.java
new file mode 100644
index 0000000..4ecbaf0
--- /dev/null
+++ b/src/org/helioviewer/jhv/io/DataSources.java
@@ -0,0 +1,95 @@
+package org.helioviewer.jhv.io;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.helioviewer.jhv.JHVGlobals;
+
+@SuppressWarnings("serial")
+public class DataSources {
+
+ static final Set SupportedObservatories = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ "SOHO", "SDO", "STEREO_A", "STEREO_B", "PROBA2", "ROB-USET", "ROB-Humain", "NSO-GONG", "NSO-SOLIS", "Kanzelhoehe", "NRH", "Yohkoh", "Hinode", "TRACE"
+ )));
+
+ private static final HashMap> serverSettings = new HashMap>() {
+ {
+ put("ROB", new HashMap() {
+ {
+ put("API.getDataSources", "http://swhv.oma.be/hv/api/?action=getDataSources&verbose=true&enable=[STEREO_A,STEREO_B,PROBA2]");
+ put("API.getJP2Image", "http://swhv.oma.be/hv/api/index.php?action=getJP2Image&");
+ put("API.getJPX", "http://swhv.oma.be/hv/api/index.php?action=getJPX&");
+ put("label", "Royal Observatory of Belgium");
+ put("schema", "/data/sources_v1.0.json");
+ put("availability.images", "http://swhv.oma.be/availability/images/availability/availability.html");
+ }
+ });
+ put("IAS", new HashMap() {
+ {
+ put("API.getDataSources", "http://helioviewer.ias.u-psud.fr/helioviewer/api/?action=getDataSources&verbose=true&enable=[TRACE,Hinode,Yohkoh,STEREO_A,STEREO_B,PROBA2]");
+ put("API.getJP2Image", "http://helioviewer.ias.u-psud.fr/helioviewer/api/index.php?action=getJP2Image&");
+ put("API.getJPX", "http://helioviewer.ias.u-psud.fr/helioviewer/api/index.php?action=getJPX&");
+ put("label", "Institut d'Astrophysique Spatiale");
+ put("schema", "/data/sources_v1.0.json");
+ }
+ });
+ put("GSFC", new HashMap() {
+ {
+ put("API.getDataSources", "https://api.helioviewer.org/v2/getDataSources/?verbose=true&enable=[TRACE,Hinode,Yohkoh,STEREO_A,STEREO_B,PROBA2]");
+ put("API.getJP2Image", "https://api.helioviewer.org/v2/getJP2Image/?");
+ put("API.getJPX", "https://api.helioviewer.org/v2/getJPX/?");
+ put("label", "Goddard Space Flight Center");
+ put("schema", "/data/sources_v1.0.json");
+ }
+ });
+ /*
+ put("GSFC SCI Test", new HashMap() {
+ {
+ put("API.getDataSources", "http://helioviewer.sci.gsfc.nasa.gov/api.php?action=getDataSources&verbose=true&enable=[TRACE,Hinode,Yohkoh,STEREO_A,STEREO_B,PROBA2]");
+ put("API.getJP2Image", "http://helioviewer.sci.gsfc.nasa.gov/api.php?action=getJP2Image&");
+ put("API.getJPX", "http://helioviewer.sci.gsfc.nasa.gov/api.php?action=getJPX&");
+ put("label", "Goddard Space Flight Center SCI Test");
+ put("schema", "/data/sources_v1.0.json");
+ }
+ });
+ put("GSFC NDC Test", new HashMap() {
+ {
+ put("API.getDataSources", "http://gs671-heliovw7.ndc.nasa.gov/api.php?action=getDataSources&verbose=true&enable=[TRACE,Hinode,Yohkoh,STEREO_A,STEREO_B,PROBA2]");
+ put("API.getJP2Image", "http://gs671-heliovw7.ndc.nasa.gov/api.php?action=getJP2Image&");
+ put("API.getJPX", "http://gs671-heliovw7.ndc.nasa.gov/api.php?action=getJPX&");
+ put("label", "Goddard Space Flight Center NDC Test");
+ put("schema", "/data/sources_v1.0.json");
+ }
+ });
+ put("LOCALHOST", new HashMap() {
+ {
+ put("API.getDataSources", "http://localhost:8080/helioviewer/api/?action=getDataSources&verbose=true&enable=[STEREO_A,STEREO_B,PROBA2]");
+ put("API.getJP2Image", "http://localhost:8080/helioviewer/api/index.php?action=getJP2Image&");
+ put("API.getJPX", "http://localhost:8080/helioviewer/api/index.php?action=getJPX&");
+ put("schema", "/data/sources_v1.0.json");
+ put("label", "Localhost");
+ }
+ });
+ */
+ }
+ };
+
+ public static Set getServers() {
+ return serverSettings.keySet();
+ }
+
+ public static String getServerSetting(String server, String setting) {
+ Map settings = serverSettings.get(server);
+ return settings == null ? null : settings.get(setting);
+ }
+
+ public static void loadSources() {
+ for (String serverName : serverSettings.keySet())
+ JHVGlobals.getExecutorService().execute(new DataSourcesTask(serverName));
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/io/DataSourcesTask.java b/src/org/helioviewer/jhv/io/DataSourcesTask.java
new file mode 100644
index 0000000..ffca720
--- /dev/null
+++ b/src/org/helioviewer/jhv/io/DataSourcesTask.java
@@ -0,0 +1,74 @@
+package org.helioviewer.jhv.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.everit.json.schema.Schema;
+import org.everit.json.schema.loader.SchemaLoader;
+import org.helioviewer.jhv.base.DownloadStream;
+import org.helioviewer.jhv.base.FileUtils;
+import org.helioviewer.jhv.base.JSONUtils;
+import org.helioviewer.jhv.base.logging.Log;
+import org.helioviewer.jhv.base.time.TimeUtils;
+import org.helioviewer.jhv.threads.JHVWorker;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+public class DataSourcesTask extends JHVWorker {
+
+ private final String url;
+ private final String schemaName;
+
+ public DataSourcesTask(String server) {
+ url = DataSources.getServerSetting(server, "API.getDataSources");
+ schemaName = DataSources.getServerSetting(server, "schema");
+ setThreadName("MAIN--DataSources");
+ }
+
+ @Override
+ protected Void backgroundWork() {
+ while (true) {
+ Schema schema = null;
+ try (InputStream is = FileUtils.getResourceInputStream(schemaName)) {
+ JSONObject rawSchema = new JSONObject(new JSONTokener(is));
+ SchemaLoader schemaLoader = SchemaLoader.builder().schemaJson(rawSchema).addFormatValidator(new TimeUtils.SQLDateTimeFormatValidator()).build();
+ schema = schemaLoader.load().build();
+ } catch (Exception e) {
+ Log.error("Could not load the JSON schema: ", e);
+ }
+
+ try {
+ JSONObject json = JSONUtils.getJSONStream(new DownloadStream(url).getInput());
+/*
+ if (url.contains("helioviewer.org")) {
+ json.getJSONObject("PROBA2").getJSONObject("children").getJSONObject("SWAP").getJSONObject("children").remove("174");
+ JSONObject o = new JSONObject( "{\"sourceId\":32,\"layeringOrder\":1,\"name\":\"174\u205fÅ\",\"nickname\":\"SWAP 174\",\"start\":\"2010-01-04 17:00:50\",\"description\":\"174 Ångström extreme ultraviolet\",\"end\":\"2017-03-21 10:23:31\",\"label\":\"Measurement\"} ");
+ json.getJSONObject("PROBA2").getJSONObject("children").getJSONObject("SWAP").getJSONObject("children").put("174", o);
+ }
+*/
+ if (schema != null)
+ schema.validate(json);
+ return null;
+ } catch (IOException e) {
+ try {
+ // Log.error(e);
+ Thread.sleep(5000);
+ } catch (InterruptedException e1) {
+ Log.error(e1);
+ break;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void done() {
+ try {
+ get(); // recover background exceptions
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/threads/JHVExecutor.java b/src/org/helioviewer/jhv/threads/JHVExecutor.java
new file mode 100644
index 0000000..9f3c14a
--- /dev/null
+++ b/src/org/helioviewer/jhv/threads/JHVExecutor.java
@@ -0,0 +1,98 @@
+package org.helioviewer.jhv.threads;
+
+import java.lang.ref.WeakReference;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class JHVExecutor {
+
+/*
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import sun.awt.AppContext;
+
+ public static synchronized void setSwingWorkersExecutorService(int MAX_WORKER_THREADS) {
+
+ final AppContext appContext = AppContext.getAppContext();
+ ExecutorService executorService = (ExecutorService) appContext.get(SwingWorker.class);
+ if (executorService == null) {
+ executorService = new ThreadPoolExecutor(MAX_WORKER_THREADS / 2, MAX_WORKER_THREADS,
+ 10L, TimeUnit.MINUTES, new LinkedBlockingQueue(),
+ new JHVThread.NamedThreadFactory("JHVWorker-Swing-"));
+ shutdownOnDisposal(appContext, executorService);
+
+ appContext.put(SwingWorker.class, executorService);
+ }
+ }
+
+ private static void shutdownOnDisposal(AppContext appContext, final ExecutorService es) {
+ // Don't use ShutdownHook here as it's not enough. We should track
+ // AppContext disposal instead of JVM shutdown, see 6799345 for details
+ appContext.addPropertyChangeListener(AppContext.DISPOSED_PROPERTY_NAME,
+ new PropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent pce) {
+ boolean disposed = (Boolean) pce.getNewValue();
+ if (disposed) {
+ final WeakReference executorServiceRef = new WeakReference(es);
+ final ExecutorService executorService = executorServiceRef.get();
+ if (executorService != null) {
+ AccessController.doPrivileged(
+ new PrivilegedAction() {
+ @Override
+ public Void run() {
+ executorService.shutdown();
+ return null;
+ }
+ }
+ );
+ }
+ }
+ }
+ }
+ );
+ }
+*/
+
+ public static ExecutorService getJHVWorkersExecutorService(String name, int MAX_WORKER_THREADS) {
+ ExecutorService executorService = new ThreadPoolExecutor(MAX_WORKER_THREADS / 2, MAX_WORKER_THREADS,
+ 10L, TimeUnit.MINUTES, new LinkedBlockingQueue<>(), new JHVThread.NamedThreadFactory("JHVWorker-" + name)) {
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ super.afterExecute(r, t);
+ JHVThread.afterExecute(r, t);
+ }
+ };
+ shutdownOnDisposal(executorService);
+ return executorService;
+ }
+
+ private static void shutdownOnDisposal(ExecutorService es) {
+ Runnable shutdownHook =
+ new Runnable() {
+ final WeakReference executorServiceRef = new WeakReference<>(es);
+ public void run() {
+ ExecutorService executorService = executorServiceRef.get();
+ if (executorService != null) {
+ AccessController.doPrivileged(
+ (PrivilegedAction) () -> {
+ executorService.shutdown();
+ return null;
+ });
+ }
+ }
+ };
+
+ AccessController.doPrivileged(
+ (PrivilegedAction) () -> {
+ Runtime.getRuntime().addShutdownHook(new Thread(shutdownHook));
+ return null;
+ });
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/threads/JHVThread.java b/src/org/helioviewer/jhv/threads/JHVThread.java
new file mode 100644
index 0000000..9e94f38
--- /dev/null
+++ b/src/org/helioviewer/jhv/threads/JHVThread.java
@@ -0,0 +1,49 @@
+package org.helioviewer.jhv.threads;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+
+import org.jetbrains.annotations.NotNull;
+
+public class JHVThread {
+
+ public static void afterExecute(Runnable r, Throwable t) {
+ if (t == null && r instanceof Future>) {
+ try {
+ Future> future = (Future>) r;
+ if (future.isDone()) {
+ future.get();
+ }
+ } catch (CancellationException e) {
+ t = e;
+ } catch (ExecutionException e) {
+ t = e.getCause();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt(); // ??? ignore/reset
+ }
+ }
+ if (t != null) {
+ t.printStackTrace();
+ }
+ }
+
+ // this creates daemon threads
+ public static class NamedThreadFactory implements ThreadFactory {
+
+ final String name;
+
+ public NamedThreadFactory(String _name) {
+ name = _name;
+ }
+
+ @Override
+ public Thread newThread(@NotNull Runnable r) {
+ Thread thread = new Thread(r, name);
+ thread.setDaemon(true);
+ return thread;
+ }
+ }
+
+}
diff --git a/src/org/helioviewer/jhv/threads/JHVWorker.java b/src/org/helioviewer/jhv/threads/JHVWorker.java
new file mode 100644
index 0000000..00d4cd3
--- /dev/null
+++ b/src/org/helioviewer/jhv/threads/JHVWorker.java
@@ -0,0 +1,33 @@
+package org.helioviewer.jhv.threads;
+
+import javax.swing.SwingWorker;
+
+public abstract class JHVWorker extends SwingWorker {
+
+ private String name;
+
+ public String getThreadName() {
+ return name;
+ }
+
+ public void setThreadName(String _name) {
+ name = _name;
+ }
+
+ @Override
+ protected T doInBackground() {
+ String currentName = Thread.currentThread().getName();
+ if (name != null)
+ Thread.currentThread().setName("JHVWorker-" + name);
+
+ T ret = backgroundWork();
+
+ if (name != null)
+ Thread.currentThread().setName(currentName);
+
+ return ret;
+ }
+
+ protected abstract T backgroundWork();
+
+}