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.

4

2 回答 2

2

您可以通过实现自定义 SPARQL 函数并使用它而不是实际 now()函数来完成此操作。举个mock_now()例子。既然你实现了它,你就可以完全控制它的行为。

于 2017-08-15T01:21:55.110 回答
1

我正在发布我的问题的解决方案,希望它可以作为 RDF4J 下自定义 SPARQL 函数的进一步示例对其他人有所帮助。我不认为这是一个优雅的解决方案(由于我如何设置测试条件),但它确实有效并且符合我的要求。该解决方案基于http://docs.rdf4j.org/custom-sparql-functions/扩展了@jeen_broekstra 的答案...

我现在在由定义的命名空间中实现了一个自定义PREFIX soo: <http://www.disa.mil/dso/a2i/ontologies/PBSM/Sharing/SpectrumOperationsOntology#>作为调用的函数,该函数soo:spectrumOpsDateTime()可以接受三个参数或不接受任何参数。三个参数的情况允许按如下方式设置缩放的日期时间。

  • 第一个参数:xsd:boolean... 如果是使用系统时钟,如果是true使用缩放时钟false
  • 第二个参数:(xsd:dateTime如果第一个参数为真,则忽略)... 缩放时钟操作的开始日期/时间
  • 第三个参数:(xsd:double如果第一个参数为真,则忽略)... 缩放时钟速率(例如 2.0 表示缩放时钟运行得更快,实时两倍)

如果没有参数,则soo:spectrumOpsDateTime()返回缩放的日期/时间或系统日期/时间,具体取决于 Java 代码中的初始值指定的内容或最后一个三参数调用指定的内容。被测 SPARQL 和 SPIN 代码将仅使用无参数版本。测试设置查询将为特定测试设置时间条件。

这是一个示例 SPARQL 设置查询,用于设置从今天早上开始的 2 倍速度:

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) .
}

这是获取缩放日期/时间的示例 SPARQL 查询:

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

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

用于实现此自定义功能的单个类是:

/**
 * 
 */

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();
            }
        }
    }
}

在我的情况下,从 Eclipse 导出的 jar 文件在我安装的 Apache 下执行,并驻留在C:\Apache\apache-tomcat-8.5.15\webapps\rdf4j-server\WEB-INF\lib\ScaledDateTime.jar 我在执行修改时替换此 jar 文件后重新启动 Apache 服务器。

于 2017-08-22T16:28:43.883 回答