在 RDF4J 中操作 Date/Time 进行调试

Manipulate Date/Time in RDF4J for Debugging

我在 Windows 10 Professional 64 位上使用 RDF4J 2.2.1。我将有一些对 date/time 敏感的 SPIN 构造函数规则。例如,我可能想将包含 xsd:dateTime 数据类型 属性 的三元组与 SPARQL 的内置 now() 函数的输出进行比较。要调试此功能,以某种方式操纵 RDF4J 对 date/time 的感知比操纵系统时钟更方便。我知道有一些通用的商业软件(例如 Solution Soft 的 "Time Machine")通常可以操纵任何 Windows 进程的时间感知。然而,这个软件对于我们的小概念验证项目来说似乎太贵了。

我希望能够做的事情:

有没有人对如何以这种方式 date/time 为 RDF4J 进行操作提出建议?这将使我对时间敏感的 SPIN 规则的调试更加高效。我不想与我的 PC 的系统时钟发生冲突,因为许多其他事情都依赖于它。我想 运行 整个虚拟 PC 并在虚拟 PC 上调试是另一种选择,但似乎应该有更简单的方法。

谢谢。

您可以通过实施 custom SPARQL function 并使用它代替 actual now() 函数来实现此目的。例如称它为 mock_now()。由于您实施了它,因此您可以完全控制它的行为。

我发布了我的问题解决方案,希望它可以作为 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(如果第一个参数为真则忽略)...缩放时钟操作的起始date/time
  • 第三个参数:xsd:double(如果第一个参数为真则忽略)...比例时钟速率(例如 2.0 表示比例时钟运行得更快,两倍于实时)

如果没有参数,soo:spectrumOpsDateTime() returns 缩放 date/time 或系统 date/time 取决于 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 查询示例,用于获取缩放 date/time:

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

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

用于实现此自定义函数的单个class是:

/**
 * 
 */

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 服务器。