/*
* -----------------------------------------------------------------------
* Copyright © 2013-2015 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (NetTimeConnector.java) is part of project Time4J.
*
* Time4J 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.
*
* Time4J 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 Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.clock;
import net.time4j.Moment;
import net.time4j.SI;
import net.time4j.SystemClock;
import net.time4j.scale.TimeScale;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.util.ServiceLoader;
/**
* <p>Represents an abstract connection object to an internet time server. </p>
*
* <p>A socket connection can be established by {@code connect()} and then the difference
* between the local computer clock using {@code SystemClock.MONOTONIC} and the net time
* can be queried. Next time queries of this clock are based on this difference
* and the local clock - until the next CONNECT. </p>
*
* @param <C> generic configuration type
* @author Meno Hochschild
* @see SystemClock#MONOTONIC
*/
/*[deutsch]
* <p>Stellt ein abstraktes Verbindungsobjekt zu einem Uhrzeit-Server dar. </p>
*
* <p>Mit Hilfe von {@code connect()} wird eine Socket-Verbindung aufgebaut, der Uhrzeit-Server
* abgefragt und dann die Differenz zwischen lokaler Rechneruhr ({@code SystemClock.MONOTONIC})
* und Netzzeit notiert. Weitere normale Zeitanfragen an diese Klasse basieren bis zum nächsten
* CONNECT auf dieser Zeitdifferenz und der lokalen Rechneruhr. </p>
*
* @param <C> generischer Konfigurationstyp
* @author Meno Hochschild
* @see SystemClock#MONOTONIC
*/
public abstract class NetTimeConnector<C extends NetTimeConfiguration>
extends AbstractClock {
//~ Statische Felder/Initialisierungen --------------------------------
private static final int MIO = 1000000;
//~ Instanzvariablen --------------------------------------------------
private final Moment startMoment;
private final C defaultNTC;
private volatile ConnectionResult result;
private volatile PrintWriter writer;
private volatile C ntc;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Creates a configured instance. </p>
*
* @param ntc Verbindungskonfiguration
*/
/*[deutsch]
* <p>Erzeugt eine konfigurierte Instanz. </p>
*
* @param ntc Verbindungskonfiguration
*/
protected NetTimeConnector(C ntc) {
super();
if (ntc == null) {
throw new NullPointerException("Missing configuration parameters.");
}
this.startMoment = SystemClock.MONOTONIC.currentTime();
this.defaultNTC = ntc;
this.result = null;
this.writer = null;
this.ntc = ntc;
}
//~ Methoden ----------------------------------------------------------
/**
* <p>Yields the current time after a connection has been established
* at least once. </p>
*
* <p>If there was no connection yet then this method just displays
* the time of clock construction. Note that this method is still
* sensible for any local clock change triggered by the underlying
* operating system. If an application needs a fresh internet time then
* following code can be used instead (causing network traffic): </p>
*
* <pre>
* NetTimeConnector<?> clock = ...;
* clock.connect();
* Moment currentTime = clock.getLastConnectionTime();
* </pre>
*
* @return Moment
* @see #isRunning()
*/
/*[deutsch]
* <p>Liefert die aktuelle Zeit, nachdem eine Verbindung wenigstens
* einmal hergestellt wurde. </p>
*
* <p>Hat es noch keine Verbindung gegeben, dann zeigt diese Methode
* lediglich die Zeit an, zu der diese Uhr konstruiert wurde. Zu
* beachten: Diese Methode reagiert empfindlich auf jedwede Änderung
* der lokalen Uhr, die vom zugrundeliegenden Betriebssystem verursacht
* wird. Wenn eine Anwendung direkt die Internet-Zeit benötigt,
* dann kann stattdessen folgender Code verwendet werden (verursacht
* eine Netzwerkverbindung): </p>
*
* <pre>
* NetTimeConnector<?> clock = ...;
* clock.connect();
* Moment currentTime = clock.getLastConnectionTime();
* </pre>
*
* @return Moment
* @see #isRunning()
*/
@Override
public Moment currentTime() {
final ConnectionResult cr = this.result;
if (cr == null) {
return this.startMoment;
}
long localMicros = SystemClock.MONOTONIC.realTimeInMicros();
long amount =
localMicros
+ cr.getActualOffset(localMicros)
- extractMicros(cr.lastMoment);
return cr.lastMoment.plus(amount * 1000, SI.NANOSECONDS);
}
/**
* <p>The clock is running as soon as there was established a connection
* at least once. </p>
*
* <p>If there is not yet any connection then this clock will only display
* the time of its construction. </p>
*
* @return boolean
* @see #connect()
*/
/*[deutsch]
* <p>Die Uhr läuft, sobald wenigstens einmal eine Verbindung
* hergestellt wurde. </p>
*
* <p>Ist noch keine Verbindung hergestellt worden, zeigt diese Instanz
* solange nur die Zeit an, zu der sie konstruiert wurde. </p>
*
* @return boolean
* @see #connect()
*/
public boolean isRunning() {
return (this.result != null);
}
/**
* <p>Queries a time server for the current time. </p>
*
* <p>The result can then be achieved by the method {@code currentTime()}
* which is based on the network offset calculated in last connection.
* A connection to the server only happens in this method and not in
* the method {@code currentTime()}. </p>
*
* @throws IOException if connection fails or in case of any
* inconsistent server answers
* @see #currentTime()
*/
/*[deutsch]
* <p>Fragt einen Server nach der aktuellen Uhrzeit ab. </p>
*
* <p>Das Ergebnis kann dann mit Hilfe der Methode {@code currentTime()}
* abgelesen werden, welche auf dem durch die letzte Abfrage gewonnenen
* Netzwerk-Offset basiert. Somit findet eine Verbindung zum Server nur
* hier und nicht in der besagten Zeitermittlungsmethode statt. </p>
*
* @throws IOException bei Verbindungsfehlern oder inkonsistenten Antworten
* @see #currentTime()
*/
public final void connect() throws IOException {
try {
Moment moment = this.doConnect();
long localMicros = SystemClock.MONOTONIC.realTimeInMicros();
final ConnectionResult cr = this.result;
long currentOffset = (
(cr == null)
? Long.MIN_VALUE : cr.getActualOffset(localMicros));
this.result =
new ConnectionResult(
moment,
localMicros,
currentOffset,
this.getNetTimeConfiguration().getClockShiftWindow()
);
} catch (ParseException pe) {
throw new IOException("Cannot read server reply.", pe);
}
}
/**
* <p>Queries the configuration parameters to be used for the next
* connection. </p>
*
* <p>The configuration will be determined by a {@code ServiceLoader}.
* If not available then this connector will just choose the configuration
* which was given during construction of this instance. Any possible
* {@code IllegalStateException} will be logged to the error console.
* This method delegates to {@code loadNetTimeConfiguration()}. </p>
*
* @return {@code true} if successful else {@code false}
* @see #loadNetTimeConfiguration()
*/
/*[deutsch]
* <p>Liest die Konfiguration für den nächsten Verbindungsaufbau
* neu ein. </p>
*
* <p>Die Verbindungskonfiguration wird über den SPI-Service
* {@link NetTimeConfiguration} ermittelt. Falls nicht vorhanden,
* wird die Standardkonfiguration gewählt, die bei Konstruktion
* dieser Instanz angegeben wurde. Wenn das Laden der
* Konfiguration eine {@code IllegalStateException} verursacht, wird
* diese abgefangen und auf die Fehlerkonsole geloggt. Das eigentliche
* Laden geschieht intern mittels {@code loadNetTimeConfiguration()}. </p>
*
* @return {@code true} wenn erfolgreich konfiguriert, sonst {@code false}
* @see #loadNetTimeConfiguration()
*/
public final boolean reconfigure() {
try {
this.ntc = this.loadNetTimeConfiguration();
return true;
} catch (IllegalStateException ex) {
ex.printStackTrace(System.err);
return false;
}
}
/**
* <p>Yields the current configuration parameters. </p>
*
* @return configuration object
* @see #reconfigure()
*/
/*[deutsch]
* <p>Liefert die aktuell geladene Konfiguration. </p>
*
* @return Konfigurationsobjekt
* @see #reconfigure()
*/
public C getNetTimeConfiguration() {
return this.ntc;
}
/**
* <p>Installs a logging stream for any messages during connection. </p>
*
* @param out output stream ({@code null} disables logging)
*/
/*[deutsch]
* <p>Installiert einen Strom zum Loggen. </p>
*
* @param out Ausgabestrom ({@code null} schaltet das Loggen ab)
*/
public void setLogWriter(PrintWriter out) {
this.writer = out;
}
/**
* <p>Determines if the internal logging is enabled. </p>
*
* @return {@code true} if there is any installed log writer
* else {@code false}
* @see #setLogWriter(PrintWriter)
*/
/*[deutsch]
* <p>Ermittelt, ob das interne Logging eingeschaltet ist. </p>
*
* @return {@code true} wenn ein Log-Writer existiert, sonst {@code false}
* @see #setLogWriter(PrintWriter)
*/
public boolean isLogEnabled() {
return (this.writer != null);
}
/**
* <p>Yields the time of last connection. </p>
*
* @return moment of last connection or {@code null} if there was not
* any connection yet
* @see #connect()
*/
/*[deutsch]
* <p>Liefert die während des letzten Verbindungsaufbaus ermittelte
* Netz-Zeit. </p>
*
* @return moment of last connection or {@code null} if there was not
* any connection yet
* @see #connect()
*/
public Moment getLastConnectionTime() {
final ConnectionResult cr = this.result;
return ((cr == null) ? null : cr.lastMoment);
}
/**
* <p>Yields the last offset between net time and local time in
* microseconds. </p>
*
* @return offset in microseconds ({@code 0} if there was not any
* connection yet)
* @see #connect()
*/
/*[deutsch]
* <p>Liefert die zuletzt ermittelte Differenz zwischen Netz-Zeit und
* lokaler Zeit in Mikrosekunden. </p>
*
* @return offset in microseconds ({@code 0} if there was not any
* connection yet)
* @see #connect()
*/
public long getLastOffsetInMicros() {
return this.getLastOffset(SystemClock.MONOTONIC.realTimeInMicros());
}
/**
* <p>Will be called by {@code connect()}. </p>
*
* @return queried current time
* @throws IOException in case of any connection failure
* @throws ParseException if the server reply is not readable
* @see #connect()
*/
/*[deutsch]
* <p>Wird von {@code connect()} aufgerufen. </p>
*
* @return gelesener Zeitpunkt
* @throws IOException bei Verbindungsfehlern
* @throws ParseException wenn die Antwort des Servers unlesbar ist
* @see #connect()
*/
protected abstract Moment doConnect() throws IOException, ParseException;
/**
* <p>Loads the configuration parameters via a {@code ServiceLoader}. </p>
*
* <p>Subclasses should always start by calling
* {@code super.loadNetTimeConfiguration()} if this
* method is overridden. </p>
*
* @return loaded configuration parameters
* @throws IllegalStateException in case of any configuration error
* @see #reconfigure()
*/
/*[deutsch]
* <p>Laden der Konfiguration über einen SPI-Service. </p>
*
* <p>Subklassen sollten zuerst mit {@code super()} diese Methode aufrufen,
* wenn sie sie überschreiben. </p>
*
* @return geladene Konfiguration
* @throws IllegalStateException bei Konfigurationsfehlern
* @see #reconfigure()
*/
protected synchronized C loadNetTimeConfiguration() {
ServiceLoader<C> sl = ServiceLoader.load(this.getConfigurationType());
C loaded = null;
for (C cfg : sl) {
if (cfg != null) {
loaded = cfg;
break;
}
}
if (loaded == null) {
loaded = this.defaultNTC;
}
String addr = loaded.getTimeServerAddress();
int port = loaded.getTimeServerPort();
if (
(addr == null)
|| addr.trim().isEmpty()
) {
throw new IllegalStateException("Missing time server address.");
} else if (loaded.getConnectionTimeout() < 0) {
throw new IllegalStateException("Negative time out.");
} else if ((port < 0) || (port > 65536)) {
throw new IllegalStateException("Port out of range: " + port);
} else if (loaded.getClockShiftWindow() < 0) {
throw new IllegalStateException("Clock shift window is negative.");
}
return loaded;
}
/**
* <p>Logs given message if a {@code PrintWriter} has been installed. </p>
*
* @param prefix optional prefix
* @param message message to be logged
* @see #setLogWriter(PrintWriter)
*/
/*[deutsch]
* <p>Loggt die angegebene Nachricht, falls ein {@code PrintWriter}
* installiert ist. </p>
*
* @param prefix optionales Präfix vor der eigentlichen Meldung
* @param message zu loggende Meldung
* @see #setLogWriter(PrintWriter)
*/
protected void log(
String prefix, // nullable
String message
) {
final PrintWriter out = this.writer;
if (out != null) {
if (prefix == null) {
out.println(message);
} else {
out.println(prefix + message);
}
}
}
/**
* <p>Yields the configuration type. </p>
*
* @return configuration type
*/
/*[deutsch]
* <p>Liefert den Typ der Verbindungskonfiguration. </p>
*
* @return Konfigurationstyp
*/
protected abstract Class<C> getConfigurationType();
/**
* <p>Liefert die aktuelle Differenz zwischen Netz-Zeit und lokaler Zeit in Mikrosekunden. </p>
*
* @param micros aktuelle lokale Zeit in Mikrosekunden
* @return Mikrosekunden-Offset ({@code 0}, wenn noch keine Verbindung hergestellt wurde)
*/
long getLastOffset(long micros) {
final ConnectionResult cr = this.result;
return ((cr == null) ? 0 : cr.getActualOffset(micros));
}
private static long extractMicros(Moment time) {
return time.getElapsedTime(TimeScale.UTC) * MIO + time.getNanosecond(TimeScale.UTC) / 1000;
}
//~ Innere Klassen ----------------------------------------------------
private static class ConnectionResult {
//~ Instanzvariablen ----------------------------------------------
private final Moment lastMoment;
private final long startTime;
private final long startOffset;
private final long endOffset;
private final int window;
//~ Konstruktoren -------------------------------------------------
ConnectionResult(
Moment time,
long localMicros,
long startOffset,
int window
) {
super();
this.lastMoment = time;
this.startTime = localMicros;
this.startOffset = startOffset;
this.endOffset = (extractMicros(time) - localMicros);
this.window = window * MIO;
}
// Ermittelt den aktuellen Offset in maximal Mikrosekundengenauigkeit
long getActualOffset(long micros) {
if (
(this.window == 0)
|| (this.startOffset <= this.endOffset)
|| (micros - this.startTime >= this.window)
) {
return this.endOffset; // sofortige Anpassung
}
double t = Math.max(0.0, micros - this.startTime);
double modulation = (1 + Math.cos(Math.PI * t / this.window)) / 2;
return Math.round(
this.endOffset
+ modulation * (this.startOffset - this.endOffset)
);
}
}
}