1

I'm using RDF4J 2.2.1 on Windows 10 Professional 64-bit. I will have some SPIN constructor rules which are sensitive to date/time. For example, I may want to compare a triple containing an xsd:dateTime datatype property to the output of SPARQL's built-in now() function. To debug this functionality, it would be convenient to manipulate RDF4J's perception of date/time somehow rather than manipulating the system clock. I'm aware that there is general commercial software (e.g. Solution Soft's "Time Machine") that can generally manipulate the perception of time for any Windows process. However, this software appears to be far too expensive for our little proof-of-concept project.

What I'd like to be able to do:

  • Set RDF4J's date/time to some arbitrary date/time value.
  • Have RDF4J's date/time proceed at real time speed or at some programmable faster speed during debugging.

Does anyone have suggestions for how to manipulate in this manner date/time for RDF4J? It would make my debugging of time-sensitive SPIN rules much more efficient. I'd prefer not to fight my PC's system clock since many other things depend on it. I suppose that running an entire virtual PC and debugging on the virtual PC is another option, but it seems there should be a simpler way.

Thanks.

Greg Cox
  • 287
  • 1
  • 12

2 Answers2

2

You could accomplish this by implementing a custom SPARQL function and using that instead of the actual now() function. Call it mock_now() for example. Since you implement it, you have full control over its behavior.

Jeen Broekstra
  • 21,642
  • 4
  • 51
  • 73
  • 1
    Yes, and I could have it be a simple wrapper for the actual `now()` function when not debugging. Thanks! – Greg Cox Aug 16 '17 at 01:21
  • I'm trying to implement your suggestion. I'm having trouble with this step "Once you have a proper JAR file, you need to add it the runtime classpath of your RDF4J project (or if you’re aiming to use this in an RDF4J Server, add it to the RDF4J Server webapp classpath and restart). After that, you’re done: RDF4J should automatically pick up your new custom function, you can from now on use it in your SPARQL queries." I have the server case, and I'm running under Apache. Where is the RDF4J Server webapp classpath set? An environment variable? – Greg Cox Aug 18 '17 at 21:37
  • It depends on the servlet container you use, but in the case of Apache Tomcat you can either add it to 'tomcat/shared/lib', or just add it to 'web-apps/rdf4j-server/WEB-INF/lib'. As long as it is installed somewhere where the classloader picks it up. – Jeen Broekstra Aug 19 '17 at 03:56
  • Thanks again @jeen-broekstra, I've already tried putting my jar file in 'C:\Apache\apache-tomcat-8.5.15\webapps\rdf4j-server\WEB-INF\lib' and re-starting apache, so I must not be generating the jar file correctly. I'll have another look at the jar. If I can't get that working, I'll post my situation with complete details. I'd want to isolate the function from the rest of my code to avoid having so much clutter in my jar file first. – Greg Cox Aug 21 '17 at 14:41
  • 1
    I found my error, @jeen-broekestra. I missed one level of directory structure for my org.eclipse.rdf4j.query.algebra.evaluation.function.Function file, forgetting the "services" subdirectory. After correcting that, my new custom function http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#spectrumOpsDateTime works. Thanks! – Greg Cox Aug 21 '17 at 15:17
1

I'm posting my solution to my question in hopes it might help others as a further example of a custom SPARQL function under RDF4J. I don't hold this out as en elegant solution (due to how I set test conditions), but it does work and meets my requirements. This solution extends the answer from @jeen_broekstra based on http://docs.rdf4j.org/custom-sparql-functions/...

I now have a custom implemented in the namespace defined by PREFIX soo: <http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#>as a function called soo:spectrumOpsDateTime() which can take either three or no arguments. The three arguments case allows setting the scaled date time as follows.

  • First argument: xsd:boolean... use system clock if true or use scaled clock if false
  • Second argument: xsd:dateTime (ignored if first argument is true)... the starting date/time for scaled clock operation
  • Third argument: xsd:double (ignored if first argument is true)... the scaled clock rate (e.g. 2.0 means the scaled clock runs faster, at twice real time)

If there are no arguments, soo:spectrumOpsDateTime() returns the scaled date/time or the system date/time depending on what the initial values in the Java code specify or what the last three-argument call specified. The SPARQL and SPIN code under test will use only the no-argument version. Test setup queries will set up the time conditions for particular tests.

Here's an example SPARQL setup query to set up a 2x speed starting this morning:

PREFIX soo: <http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#>

SELECT DISTINCT *
WHERE {
  BIND(soo:spectrumOpsDateTime("false"^^xsd:boolean, "2017-08-22T10:49:21.019-05:00"^^xsd:dateTime, "2.0"^^xsd:double) AS ?testDateTime) .
}

Here's an example SPARQL query to get the scaled date/time:

PREFIX soo: <http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#>

SELECT DISTINCT *
WHERE {
  BIND(soo:spectrumOpsDateTime() AS ?testDateTime) .
}

The single class used to implement this custom function is:

/**
 * 
 */

package mil.disa.dso.spo.a2i.nsc.sharing2025.scaledDateTime;

import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.query.algebra.evaluation.ValueExprEvaluationException;
import org.eclipse.rdf4j.query.algebra.evaluation.function.Function;

/**
 * Class for generating a configurable date/time clock that can either be a pass-through of the
 * system clock or a scaled clock starting at a specified date/time running at a specified
 * rate from that specified time (first call). 
 * @author Greg Cox of Roberson and Associates &copy Copyright 2017 Roberson and Associates, All Right Reserved
 *
 */
public class DateTimeGenerator implements Function {
    private static final String thisClassName = "RDF4JCustomSPARQLFunction." + DateTimeGenerator.class.getSimpleName();
    private static final String thisClassFullName = DateTimeGenerator.class.getName();
    private static final boolean errorMessages = true;
    private static final boolean verboseMessages = true;

    private double clockPace = 2.0;                     // the speed of the clock, 1.0 is real time, 2.0 is 2x real time (double speed)
    private boolean useSystemClock = false;             // flag to indicate whether to use scaled clock or pass through the system clock

    private ZonedDateTime startingRealDateTime = null;  // the real time stamp at the first call to the evaluate function
    private ZonedDateTime startingScaledDateTime =      // the scaled time stamp (starting scaled time) at the first call to the evaluate function
            ZonedDateTime.parse("2016-08-21T17:29:37.568-05:00");

    // define a constant for the namespace of custom function
    private static String NAMESPACE = "http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#";    // defined as soo: elsewhere





    // this is the evaluate function needed to implement the RDF4J Function interface
    //  it can take 0 or 3 arguments
    //  0 - get the current scaled time (starting by first call)
    //  3 - useSystemClock flag (true/false), starting date/time (xsd:dateTime), clock pace (non-negative real w/ 1.0 meaning 1sec = 1sec)
    @SuppressWarnings("unused")
    @Override
    public Value evaluate(ValueFactory valueFactory, Value... args) throws ValueExprEvaluationException {
        String thisMethodMessagePrefix = "";

        if (errorMessages || verboseMessages ) {
            String thisMethodName = ".evaluate: ";
            thisMethodMessagePrefix = thisClassName + thisMethodName;
        }


        if (args.length == 3) {
            // Three arguments --> attempting to set mode/parameters, so attempt to parse/check them
            if (verboseMessages) System.out.println(thisMethodMessagePrefix + "attempting to set scaled clock mode/parameters");

            boolean argErrFlag = false;
            boolean newUseSystemClock = false;
            String argErrMessage = "";

            // first argument should be true/false on whether to use system clock (true) or scaled clock (false)
            if (!(args[0] instanceof Literal)) {
                argErrFlag = true;
                argErrMessage += "first argument must be a literal true/false value... ";
            } else {
                String useSystemClockString = args[0].stringValue();
                if (useSystemClockString.equalsIgnoreCase("true")) {
                    if (verboseMessages) System.out.println(thisMethodMessagePrefix + "use system clock specified");
                    newUseSystemClock = true;
                } else if (useSystemClockString.equalsIgnoreCase("false")) {
                    if (verboseMessages) System.out.println(thisMethodMessagePrefix + "use scaled clock specified");
                    newUseSystemClock = false;
                }
                else {
                    argErrFlag = true;
                    argErrMessage += "first argument must be a literal true/false value... ";
                }
            }

            // second argument should be starting date/time for scaled clock (ignore if using system clock)
            ZonedDateTime startTime = null;
            if (!newUseSystemClock) { 
                if (!(args[1] instanceof Literal)) {
                    argErrFlag = true;
                    argErrMessage += "second argument must be literal xsd:dateTime value for start of scaled date/time... ";
                } else {
                    String startDateTimeString = args[1].stringValue();
                    try {
                        startTime = ZonedDateTime.parse(startDateTimeString);
                    } catch (Exception e) {
                        argErrFlag = true;
                        argErrMessage += "could not parse starting date/time... " + e.getMessage() + "... ";
                    }
                }
            }

            // third argument should be clock pace for scaled clock (ignore if using system clock)
            Double newClockPace = null;
            if (!newUseSystemClock) {
                if (!(args[2] instanceof Literal)) {
                    argErrFlag = true;
                    argErrMessage += "third argument must be literal xsd:double value for clock pace... ";
                } else {
                    String clockPaceString = args[2].stringValue();
                    try {
                        newClockPace = Double.parseDouble(clockPaceString);
                    } catch (Exception e) {
                        argErrFlag = true;
                        argErrMessage += "could not parse clock pace which should be a positive xsd:double... ";
                    }
                    if ((newClockPace != null) && (newClockPace <= 0.0)) {
                        argErrFlag = true;
                        argErrMessage += "clock pace must be positive, got " + newClockPace + "... ";
                    }
                }
            }

            // check for errors and set up the generator if no errors...
            if (argErrFlag) {
                if (errorMessages) System.err.println(thisMethodMessagePrefix + "ERROR - " + argErrMessage);
                if (errorMessages) System.err.println(thisMethodMessagePrefix + "throwing exception...");
                throw new ValueExprEvaluationException(
                        "spectrum operations time function soo:spectrumOpsDateTime() encountered errors in function arguments... " +
                                argErrMessage);
            } else if (newUseSystemClock) {
                if (verboseMessages) System.out.println(thisMethodMessagePrefix + "using unscaled system clock");
                useSystemClock = newUseSystemClock;
            } else if (!newUseSystemClock) {
                if (verboseMessages) System.out.println(thisMethodMessagePrefix + "using scaled time");
                useSystemClock = newUseSystemClock;
                startingRealDateTime = ZonedDateTime.now();
                if (verboseMessages) System.out.println(thisMethodMessagePrefix + "setting starting real time to " + startingRealDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
                if (verboseMessages) System.out.println(thisMethodMessagePrefix + "setting start time to " + startTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
                startingScaledDateTime = startTime;
                if (verboseMessages) System.out.println(thisMethodMessagePrefix + "setting clock pace to " + String.format("%5.2f", newClockPace * 100.0) + "%");
                clockPace = newClockPace;
            }

        } else if (args.length != 0) {  // can only have no arguments or three arguments...
            throw new ValueExprEvaluationException(
                    "spectrum operations time function soo:spectrumOpsDateTime() requires "
                            + "zero arguments or three arguments, got "
                            + args.length + " arguments");
        }

        // now run the generator and return the result...

        IRI xsdDateTimeIRI = valueFactory.createIRI("http://www.w3.org/2001/XMLSchema#dateTime");  // long-form equivalent to xsd:dateTime

        if (useSystemClock) {
            String unscaledTimeString = millisTrailingZeroes(ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
            return valueFactory.createLiteral(unscaledTimeString, xsdDateTimeIRI);
        } else {
            errString = null;
            String scaledTimeString = millisTrailingZeroes(getScaledDateTime().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
            if (scaledTimeString == null) {
                if (errorMessages) System.err.println(thisMethodMessagePrefix + "ERROR - scaled time returned null");
                if (errorMessages) System.err.println(thisMethodMessagePrefix + "thowing exception...");
                throw new ValueExprEvaluationException("could not generate valid scaled time string" + ((errString == null) ? "" : "... " + errString));
            }
            return valueFactory.createLiteral(scaledTimeString, xsdDateTimeIRI);
        }
    }

    private static String errString = null;

    /**
     * Utility method to make all the millisecond fields of an <tt>ISO_OFFSET_DATE_TIME</tt> three digits by
     * adding trailing zeroes as needed.  Why? Because of trouble with various implementations interpreting
     * 1 and 2 digit milliseconds differently.  Should be standard decimal, but sometimes interpreted 
     * as number of milliseconds (e.g. .39T interpreted as 39 millieconds inststead of 390 milliseconds)
     * @param <tt>ISO_OFFSET_DATE_TIME</tt> string to check for millisecond field length
     * @return <tt>ISO_OFFSET_DATE_TIME</tt> strnig with trailing zeroes in milliseconds field
     * as require to make the field three digits or <tt>null</tt> on error
     */
    private static String millisTrailingZeroes(String isoDateTimeString) {
        if (isoDateTimeString == null) {
            errString = "DateTimeGenerator.millisTrailingZeroes: got null isoDateTimeString argument, returning null...";
            return null;
        }

        String[] ss_l1 = isoDateTimeString.split("\\.");    // Example: 2017-08-18T13:01:05.39-05:00 --> 2017-08-18T13:01:05 AND 39-05:00
        if (ss_l1.length != 2) {
            errString = "DateTImeGenerator.millisTrailingZeros: first parsing split of isoDateTimeString=" + isoDateTimeString + " by '.' got unexpected number of parts=" + ss_l1.length;
            return null;
        }

        String[] ss_l2 = ss_l1[1].split("-");               // 39-05:00 --> 39 AND 05:00
        if (ss_l2.length != 2) {
            errString = "DateTImeGenerator.millisTrailingZeros: second parsing split of " + ss_l1[1] + " by '-' got unexpected number of parts=" + ss_l2.length;
            return null;
        }

        if (ss_l2[0].length() == 1) {
            ss_l2[0] = ss_l2[0] + "00";
        } else if (ss_l2[0].length() == 2)
            ss_l2[0] = ss_l2[0] + "0";                      // 39 --> 390

        return ss_l1[0] + "." + ss_l2[0] + "-" + ss_l2[1];  // 2017-08-18T13:01:05.390-05:00
    }

    /**
     * Method to get the current scaled date time according to the state of this DateTimeGenerator.
     * If <tt>useSystemClock</tt> is <tt>true</tt>, then time is not 
     * scaled and system time is returned instead of scaled time.
     * @return scaled date time if <tt>useSystemClock</tt> is <tt>true</tt> or
     * system date time if <tt>useSystemClock</tt> is <tt>false</tt>
     */
    private ZonedDateTime getScaledDateTime() {
        ZonedDateTime scaledDateTime = null;

        if (useSystemClock) {
            scaledDateTime = ZonedDateTime.now();
        } else {
            if (startingRealDateTime == null) 
                startingRealDateTime = ZonedDateTime.now();
            long realMillisFromFirstCall = ChronoUnit.MILLIS.between(startingRealDateTime, ZonedDateTime.now());
            long scaledMillisFromFirstCall = (long) ((double) realMillisFromFirstCall * clockPace);

            scaledDateTime = ChronoUnit.MILLIS.addTo(startingScaledDateTime, scaledMillisFromFirstCall);
        }

        return scaledDateTime;
    }


    @Override
    public String getURI() {
        return NAMESPACE + "spectrumOpsDateTime";
    }

    /**
     * Test main method
     * @param args command line arguments (ignored)
     */
    @SuppressWarnings("unused")
    public static void main(String[] args) {
        String thisMethodMessagePrefix = "";

        if (errorMessages || verboseMessages ) {
            String thisMethodName = ".main: ";
            thisMethodMessagePrefix = thisClassName + thisMethodName;
        }

        DateTimeGenerator testGen = new DateTimeGenerator();

        if (verboseMessages) System.out.println(thisMethodMessagePrefix + "custom SPARQL method URI: " + testGen.getURI());
        if (verboseMessages) System.out.println(thisMethodMessagePrefix + "fully-qualified class name: " + thisClassFullName);

        ValueFactory testVF = SimpleValueFactory.getInstance();
        Value testValues[] = new Value[0];

        while (true) {

            if (verboseMessages) System.out.println(thisMethodMessagePrefix + "scaled: " + testGen.evaluate(testVF, testValues).stringValue() +
                    " current real: " + millisTrailingZeroes(ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

In my case, the jar file exported from Eclipse executes under my installation of Apache and resides at C:\Apache\apache-tomcat-8.5.15\webapps\rdf4j-server\WEB-INF\lib\ScaledDateTime.jar I restart the Apache server after replacing this jar file when I do mofifications.

Greg Cox
  • 287
  • 1
  • 12