LiteralTest.java

/*******************************************************************************
 * Copyright (c) 2020 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.model;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.YearMonth;
import java.time.temporal.ChronoField;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import org.eclipse.rdf4j.model.base.CoreDatatype;
import org.junit.jupiter.api.Test;

/**
 * Abstract {@link Literal} test suite.
 */
public abstract class LiteralTest {

	private static final String XSD = "http://www.w3.org/2001/XMLSchema#";
	private static final String RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";

	static final String XSD_BOOLEAN = XSD + "boolean";

	static final String XSD_BYTE = XSD + "byte";
	static final String XSD_SHORT = XSD + "short";
	static final String XSD_INT = XSD + "int";
	static final String XSD_LONG = XSD + "long";
	static final String XSD_FLOAT = XSD + "float";
	static final String XSD_DOUBLE = XSD + "double";
	static final String XSD_INTEGER = XSD + "integer";
	static final String XSD_DECIMAL = XSD + "decimal";

	static final String XSD_STRING = XSD + "string";

	static final String XSD_DATETIME = XSD + "dateTime";
	static final String XSD_TIME = XSD + "time";
	static final String XSD_DATE = XSD + "date";
	static final String XSD_GYEARMONTH = XSD + "gYearMonth";
	static final String XSD_GYEAR = XSD + "gYear";
	static final String XSD_GMONTHDAY = XSD + "gMonthDay";
	static final String XSD_GDAY = XSD + "gDay";
	static final String XSD_GMONTH = XSD + "gMonth";
	static final String XSD_DURATION = XSD + "duration";
	static final String XSD_DURATION_DAYTIME = XSD + "dayTimeDuration";
	static final String XSD_DURATION_YEARMONTH = XSD + "yearMonthDuration";

	static final String RDF_LANG_STRING = RDF + "langString";

	/**
	 * Creates a test literal instance.
	 *
	 * @param label the label of the literal
	 * @return a new instance of the concrete literal class under test
	 */
	protected abstract Literal literal(String label);

	/**
	 * Creates a test literal instance.
	 *
	 * @param label    the label of the literal
	 * @param language the language of the literal
	 * @return a new instance of the concrete literal class under test
	 */
	protected abstract Literal literal(String label, String language);

	/**
	 * Creates a test literal instance.
	 *
	 * @param label    the label of the literal
	 * @param datatype the datatype of the literal
	 * @return a new instance of the concrete literal class under test
	 */
	protected abstract Literal literal(String label, IRI datatype);

	/**
	 * Creates a test literal instance.
	 *
	 * @param label    the label of the literal
	 * @param datatype the CoreDatatype of the literal
	 * @return a new instance of the concrete literal class under test
	 */
	protected abstract Literal literal(String label, CoreDatatype datatype);

	/**
	 * Creates a test datatype IRI instance.
	 *
	 * @param iri the IRI of the datatype
	 * @return a new instance of the concrete datatype class under test
	 */
	protected abstract IRI datatype(String iri);

	//// Constructors //////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public final void testPlainConstructor() {

		final String label = "label";

		final Literal literal = literal(label);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_STRING);

		assertThatNullPointerException().isThrownBy(() -> literal(null));
	}

	@Test
	public final void testPlainConstructorWithLongLabel() {

		final StringBuilder label = new StringBuilder(1000000);

		for (int i = 0; i < 1000000; i++) {
			label.append(Integer.toHexString(i % 16));
		}

		final Literal literal = literal(label.toString());

		assertThat(literal.getLabel()).isEqualTo(label.toString());
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_STRING);

	}

	@Test
	public final void testTaggedConstructor() {

		final String label = "label";
		final String language = "en";

		final Literal literal = literal(label, language);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).contains(language);
		assertThat(literal.getDatatype().stringValue()).isEqualTo(RDF_LANG_STRING);

		assertThatNullPointerException().isThrownBy(() -> literal(null, (String) null));
		assertThatNullPointerException().isThrownBy(() -> literal("", (String) null));
		assertThatNullPointerException().isThrownBy(() -> literal(null, ""));
		assertThatNullPointerException().isThrownBy(() -> literal(null, (IRI) null));

		assertThatIllegalArgumentException().isThrownBy(() -> literal("", ""));

	}

	@Test
	public final void testTypedConstructor() {

		final String label = "label";
		final String datatype = "http://examplle.org/datatype";

		final Literal literal = literal(label, datatype(datatype));

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(datatype);

		assertThatNullPointerException().isThrownBy(() -> literal(null, (IRI) null));
		assertThatNullPointerException().isThrownBy(() -> literal(null, datatype(XSD_STRING)));
		assertThatNullPointerException().isThrownBy(() -> literal(null, datatype(RDF_LANG_STRING)));

		assertThatIllegalArgumentException().isThrownBy(() -> literal("", datatype(RDF_LANG_STRING)));

	}

	@Test
	public final void testTypedConstructorNullDatatype() {

		String label = "label";
		IRI datatype = null;

		Literal literal = literal(label, datatype);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getDatatype().stringValue()).isEqualTo(XSD_STRING);

	}

	//// String Value //////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testStringValue() {

		final String label = "literal";
		final String language = "en";
		final IRI datatype = datatype(XSD_DECIMAL);

		assertThat(literal(label).stringValue()).isEqualTo(label);
		assertThat(literal(label, language).stringValue()).isEqualTo(label);
		assertThat(literal(label, datatype).stringValue()).isEqualTo(label);
	}

	//// Object Values /////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testBooleanValue() {

		final IRI datatype = datatype(XSD_BOOLEAN);

		assertThat(literal("true", datatype).booleanValue()).isTrue();
		assertThat(literal("false", datatype).booleanValue()).isFalse();

		assertThat(literal("1", datatype).booleanValue()).isTrue();
		assertThat(literal("0", datatype).booleanValue()).isFalse();

		assertThat(literal("\ttrue", datatype).booleanValue()).isTrue();
		assertThat(literal("false\t", datatype).booleanValue()).isFalse();

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).booleanValue());

	}

	@Test
	public final void testByteValue() {

		final IRI datatype = datatype(XSD_BYTE);
		final Class<Byte> type = Byte.class;

		assertThat(literal("100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) 100);
		assertThat(literal("+100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) 100);
		assertThat(literal("-100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) -100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).byteValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).booleanValue());

	}

	@Test
	public final void testShortValue() {

		final IRI datatype = datatype(XSD_SHORT);
		final Class<Short> type = Short.class;

		assertThat(literal("100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) 100);
		assertThat(literal("+100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) 100);
		assertThat(literal("-100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) -100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).shortValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).shortValue());

	}

	@Test
	public final void testIntValue() {

		final IRI datatype = datatype(XSD_INT);
		final Class<Integer> type = Integer.class;

		assertThat(literal("100", datatype).intValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).intValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).intValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).intValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).intValue());

	}

	@Test
	public final void testLongValue() {

		final IRI datatype = datatype(XSD_LONG);
		final Class<Long> type = Long.class;

		assertThat(literal("100", datatype).longValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).longValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).longValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).longValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).longValue());

	}

	@Test
	public final void testFloatValue() {

		final IRI datatype = datatype(XSD_FLOAT);
		final Class<Float> type = Float.class;

		assertThat(literal("100", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).floatValue()).isInstanceOf(type).isEqualTo(-100);
		assertThat(literal("100.0", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("10e1", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);

		assertThat(literal("INF", datatype).floatValue()).isInstanceOf(type).isEqualTo(Float.POSITIVE_INFINITY);
		assertThat(literal("-INF", datatype).floatValue()).isInstanceOf(type).isEqualTo(Float.NEGATIVE_INFINITY);
		assertTrue(Float.isNaN(literal("NaN", datatype).floatValue()));

		// assertThatIllegalArgumentException().as("not normalized")
		// .isThrownBy(() -> literal("\t100", datatype).floatValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).floatValue());

	}

	@Test
	public final void testDoubleValue() {

		final IRI datatype = datatype(XSD_DOUBLE);
		final Class<Double> type = Double.class;

		assertThat(literal("100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(-100);
		assertThat(literal("100.0", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("10e1", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);

		assertThat(literal("INF", datatype).doubleValue()).isInstanceOf(type).isEqualTo(Double.POSITIVE_INFINITY);
		assertThat(literal("-INF", datatype).doubleValue()).isInstanceOf(type).isEqualTo(Double.NEGATIVE_INFINITY);
		assertTrue(Double.isNaN(literal("NaN", datatype).doubleValue()));

		// assertThatIllegalArgumentException().as("not normalized")
		// .isThrownBy(() -> literal("\t100", datatype).doubleValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).doubleValue());

	}

	@Test
	public final void testIntegerValue() {

		final IRI datatype = datatype(XSD_INTEGER);
		final Class<BigInteger> type = BigInteger.class;

		assertThat(literal("100", datatype).integerValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).integerValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).integerValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).integerValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).integerValue());

	}

	@Test
	public final void testDecimalValue() {

		final IRI datatype = datatype(XSD_DECIMAL);
		final Class<BigDecimal> type = BigDecimal.class;

		assertThat(literal("100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100"));
		assertThat(literal("+100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100"));
		assertThat(literal("-100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("-100"));
		assertThat(literal("100.0", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100.0"));
		assertThat(literal("10e1", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("1.0e2"));

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).decimalValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).decimalValue());

	}

	@Test
	public final void testTemporalDateTimeValue() {

		final String integral = "2020-09-29T01:02:03";
		final String fractional = "2020-09-29T01:02:03.004";

		final String offset = "2020-09-29T01:02:03+05:00";
		final String zero = "2020-09-29T01:02:03Z";

		assertThat(LocalDateTime.from(literal(integral, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(LocalDateTime.parse(integral));

		assertThat(LocalDateTime.from(literal(fractional, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(LocalDateTime.parse(fractional));

		assertThat(OffsetDateTime.from(literal(offset, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(OffsetDateTime.parse(offset));

		assertThat(OffsetDateTime.from(literal(zero, XSD_DATETIME).temporalAccessorValue()))
				.isEqualTo(OffsetDateTime.parse(zero));

		Stream.of(

				"0001-01-01T00:00:00",
				"0001-01-01T00:00:00.0",
				"0001-01-01T00:00:00Z",
				"0001-01-01T00:00:00.0Z",
				"0001-01-01T00:00:00+00:00",
				"0001-01-01T00:00:00.0+00:00",
				"0001-01-01T00:00:00.0-00:00",
				"0001-01-01T00:00:00.0+14:00",
				"0001-01-01T00:00:00.0-14:00",
				"0001-05-31T00:00:00.00",
				"0001-07-31T00:00:00.00",
				"0001-08-31T00:00:00.00",
				"0001-10-31T00:00:00.00",
				"0001-12-31T00:00:00.00",
				"-0001-01-01T00:00:00",
				"1234-12-31T23:59:59",
				"1234-12-31T24:00:00",
				// "12345-12-31T24:00:00",
				// "1234-12-31T24:00:00.1234567890",
				"2004-02-29T00:00:00"

		)
				.forEach(value -> assertThatCode(() -> literal(value, XSD_DATETIME).temporalAccessorValue())
						.as(value)
						.doesNotThrowAnyException()
				);

	}

	@Test
	public final void testTemporalTimeValue() {

		final String integral = "01:02:03";
		final String fractional = "01:02:03.004";

		final String offset = "01:02:03+05:00";
		final String zero = "01:02:03Z";

		assertThat(LocalTime.from(literal(integral, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(LocalTime.parse(integral));

		assertThat(LocalTime.from(literal(fractional, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(LocalTime.parse(fractional));

		assertThat(OffsetTime.from(literal(offset, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(OffsetTime.parse(offset));

		assertThat(OffsetTime.from(literal(zero, XSD_TIME).temporalAccessorValue()))
				.isEqualTo(OffsetTime.parse(zero));
	}

	@Test
	public final void testTemporalDateValue() {

		final String local = "2020-11-14";
		final String offset = "2020-11-14+05:00";
		final String zero = "2020-11-14Z";

		assertThat(LocalDate.from(literal(local, XSD_DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(local));

		assertThat(LocalDate.from(literal(offset, XSD_DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(offset.substring(0, 10))); // OffsetDate not supported by java.time

		assertThat(LocalDate.from(literal(zero, XSD_DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(offset.substring(0, 10))); // OffsetDate not supported by java.time

	}

	@Test
	public final void testTemporalGYearMonthValue() {

		final String base = "2020-11";

		assertThat(YearMonth.from(literal(base, XSD_GYEARMONTH).temporalAccessorValue()))
				.isEqualTo(YearMonth.parse(base));

	}

	@Test
	public final void testTemporalGYearValue() {

		final String local = "2020";

		assertThat(Year.from(literal(local, XSD_GYEAR).temporalAccessorValue()))
				.isEqualTo(Year.parse(local));

	}

	@Test
	public final void testTemporalGMonthDayValue() {

		final String local = "--11-14";

		assertThat(MonthDay.from(literal(local, XSD_GMONTHDAY).temporalAccessorValue()))
				.isEqualTo(MonthDay.parse(local));

	}

	@Test
	public final void testTemporalGDayValue() {

		final String local = "---14";

		assertThat(literal(local, XSD_GDAY).temporalAccessorValue().get(ChronoField.DAY_OF_MONTH))
				.isEqualTo(14);

	}

	@Test
	public final void testTemporalGMonthValue() {

		final String local = "--11";

		assertThat(Month.from(literal(local, XSD_GMONTH).temporalAccessorValue()))
				.isEqualTo(Month.NOVEMBER);

	}

	@Test
	public final void testTemporalAccessorMalformedValue() {

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("--", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("2020-11-16T", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("2020-11-16T11:12:13.", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("malformed", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("2020-11-16T", XSD_DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("2020-11-16T11:12:13.", XSD_DATETIME).temporalAccessorValue());

		Stream.of(

				"foo", "Mon, 11 Jul 2005 09:22:29 +0200",
				"0001-01-01T00:00",
				"0001-01-01T00:00.00",
				"0001-13-01T00:00:00.00",
				"0001-01-32T00:00:00.00",
				// "0001-02-30T00:00:00.00",
				// "2005-02-29T00:00:00", // not a leap year
				// "0001-04-31T00:00:00.00",
				"0001-01-01T25:00:00.00",
				"0001-01-01T00:61:00.00",
				"0001-01-01T00:00:61.00",
				"0001-01-01T00:00.00+15:00",
				"0001-01-01T00:00.00-15:00",
				"001-01-01T00:00:00.0",
				"0001-1-01T00:00:00.0",
				"0001-01-1T00:00:00.0",
				"0001-01-01T0:00:00.0",
				"0001-01-01T00:0:00.0",
				"0001-01-01T00:00:0.0",
				"0001/01-01T00:00:00.0",
				"0001-01/01T00:00:00.0",
				"0001-01-01t00:00:00.0",
				"0001-01-01T00.00:00.0",
				"0001-01-01T00:00.00.0",
				"0001-01-01T00:00:00:0",
				"0001-01-01T00:00.00+0:00",
				"0001-01-01T00:00.00+00:0",
				"0001-jan-01T00:00:00",
				"0001-01-01T00:00:00+00:00Z",
				"0001-01-01T24:01:00", "0001-01-01T24:00:01",
				"00001-01-01T00:00:00",
				"0001-001-01T00:00:00",
				"0001-01-001T00:00:00",
				"0001-01-01T000:00:00",
				"0001-01-01T00:000:00",
				"0001-01-01T00:00:000",
				"0001-01-01T00:00:000",
				"0001-01-01T00:00:00z",
				"0001-01-01T00:00:00+05",
				"0001-01-01T00:00:00+0500",
				"0001-01-01T00:00:00GMT",
				"0001-01-01T00:00:00PST",
				"0001-01-01T00:00:00GMT+05",
				// "0000-01-01T00:00:00",
				"-0000-01-01T00:00:00",
				"+0001-01-01T00:00:00"

		)
				.forEach(value -> assertThatExceptionOfType(DateTimeException.class)
						.as(value)
						.isThrownBy(() -> literal(value, XSD_DATETIME).temporalAccessorValue())
				);

	}

	@Test
	public final void testTemporalDurationValue() {

		final String period = "P1Y2M3D";
		final String duration = "PT1H2M3.4S";

		assertThat(Period.from(literal(period, XSD_DURATION).temporalAmountValue()))
				.isEqualTo(Period.parse(period));

		assertThat(Period.from(literal("-P1Y2M3D", XSD_DURATION).temporalAmountValue()))
				.isEqualTo(Period.parse(period).negated());

		assertThat(Duration.from(literal(duration, XSD_DURATION).temporalAmountValue()))
				.isEqualTo(Duration.parse(duration));

	}

	@Test
	public final void testTemporalAmountMalformedValue() {

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("malformed", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no  components")
				.isThrownBy(() -> literal("P", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("P1Y2MT", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("negative component")
				.isThrownBy(() -> literal("P-1347M ", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time separator")
				.isThrownBy(() -> literal("P1Y1S ", XSD_DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("PT1.S", XSD_DURATION).temporalAmountValue());
	}

	@Test
	public final void testCalendarValue() throws DatatypeConfigurationException {

		final Class<XMLGregorianCalendar> type = XMLGregorianCalendar.class;

		final DatatypeFactory factory = DatatypeFactory.newInstance();

		final Function<Consumer<XMLGregorianCalendar>, XMLGregorianCalendar> setup = consumer -> {

			final XMLGregorianCalendar calendar = factory.newXMLGregorianCalendar();

			consumer.accept(calendar);

			return calendar;

		};

		assertThat(literal("2020-09-29T01:02:03.004Z", XSD_DATETIME).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
					calendar.setDay(29);
					calendar.setTime(1, 2, 3, 4);
					calendar.setTimezone(0);
				}));

		assertThat(literal("01:02:03.004", XSD_TIME).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setTime(1, 2, 3, 4);
				}));

		assertThat(literal("2020-09-29", XSD_DATE).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
					calendar.setDay(29);
				}));

		assertThat(literal("2020-09", XSD_GYEARMONTH).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
				}));

		assertThat(literal("--09-29", XSD_GMONTHDAY).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setMonth(9);
					calendar.setDay(29);
				}));

		assertThat(literal("2020", XSD_GYEAR).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
				}));

		assertThat(literal("--09", XSD_GMONTH).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setMonth(9);
				}));

		assertThat(literal("---29", XSD_GDAY).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setDay(29);
				}));

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", XSD_DATETIME).calendarValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", XSD_DATETIME).calendarValue());

	}

	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testEqualsAndHashCode() {

		final Literal plain = literal("plain");
		final Literal tagged = literal("tagged", "en");
		final Literal typed = literal("typed", datatype("http://example.org/datatype"));

		final Literal _plain = literal(plain.getLabel());
		final Literal _tagged = literal(tagged.getLabel(), tagged.getLanguage().orElse(""));
		final Literal _typed = literal(typed.getLabel(), typed.getDatatype());

		assertThat(plain).isEqualTo(plain);
		assertThat(plain).isEqualTo(_plain);

		assertThat(tagged).isEqualTo(tagged);
		assertThat(tagged).isEqualTo(_tagged);

		assertThat(typed).isEqualTo(typed);
		assertThat(typed).isEqualTo(_typed);

		assertThat(plain).isNotEqualTo(null);
		assertThat(plain).isNotEqualTo(new Object());

		assertThat(plain).isNotEqualTo(tagged);
		assertThat(plain).isNotEqualTo(typed);
		assertThat(tagged).isNotEqualTo(typed);

		assertThat(plain).isNotEqualTo(literal("other"));
		assertThat(tagged).isNotEqualTo(literal(tagged.getLabel(), "other"));
		assertThat(typed).isNotEqualTo(literal(typed.getLabel(), datatype("http://example.org/other")));

		// hashCode() should return identical values for literals for which equals() is true

		assertThat(plain.hashCode()).isEqualTo(_plain.hashCode());
		assertThat(tagged.hashCode()).isEqualTo(_tagged.hashCode());
		assertThat(typed.hashCode()).isEqualTo(_typed.hashCode());

		assertThat(tagged.hashCode())
				.as("computed according to contract")
				.isEqualTo(tagged.getLabel().hashCode()); // !!! label >> label+language+datatype

	}

	@Test
	public final void testEqualsAndHashCodeCaseInsensitiveLanguage() {

		final Literal lowercase = literal("label", "en");
		final Literal uppercase = literal("label", "EN");

		assertThat(lowercase).isEqualTo(uppercase);
		assertThat(lowercase.hashCode()).isEqualTo(uppercase.hashCode());
	}

	@Test
	public final void testEqualsAndHashCodeXSDString() {

		// in RDF 1.1, there is no distinction between plain and string-typed literals

		final Literal plain = literal("label");
		final Literal typed = literal("label", datatype(XSD_STRING));

		assertThat(plain).isEqualTo(typed);
		assertThat(plain.hashCode()).isEqualTo(typed.hashCode());
	}

	@Test
	public final void testCoreDatatypePlainConstructor() {

		String label = "label";

		Literal literal = literal(label);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getCoreDatatype()).isEqualTo(CoreDatatype.XSD.STRING);

		assertThatNullPointerException().isThrownBy(() -> literal(null));
	}

	@Test
	public final void testCoreDatatypePlainConstructorWithLongLabel() {

		StringBuilder label = new StringBuilder(1000000);

		for (int i = 0; i < 1000000; i++) {
			label.append(Integer.toHexString(i % 16));
		}

		Literal literal = literal(label.toString());

		assertThat(literal.getLabel()).isEqualTo(label.toString());
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getCoreDatatype()).isEqualTo(CoreDatatype.XSD.STRING);

	}

	@Test
	public final void testCoreDatatypeTaggedConstructor() {

		String label = "label";
		String language = "en";

		Literal literal = literal(label, language);

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).contains(language);
		assertThat(literal.getCoreDatatype()).isEqualTo(CoreDatatype.RDF.LANGSTRING);

		assertThatNullPointerException().isThrownBy(() -> literal(null, (String) null));
		assertThatNullPointerException().isThrownBy(() -> literal("", (String) null));
		assertThatNullPointerException().isThrownBy(() -> literal(null, ""));
		assertThatNullPointerException().isThrownBy(() -> literal(null, (CoreDatatype) null));

		assertThatIllegalArgumentException().isThrownBy(() -> literal("", ""));

	}

	@Test
	public final void testCoreDatatypeTypedConstructor() {

		String label = "label";
		String datatype = "http://examplle.org/datatype";

		Literal literal = literal(label, datatype(datatype));

		assertThat(literal.getLabel()).isEqualTo(label);
		assertThat(literal.getLanguage()).isNotPresent();
		assertThat(literal.getCoreDatatype()).isEqualTo(CoreDatatype.NONE);

		assertThatNullPointerException().isThrownBy(() -> literal(null, (CoreDatatype) null));
		assertThatNullPointerException().isThrownBy(() -> literal(null, CoreDatatype.XSD.STRING));
		assertThatNullPointerException().isThrownBy(() -> literal(null, CoreDatatype.RDF.LANGSTRING));

		assertThatIllegalArgumentException().isThrownBy(() -> literal("", CoreDatatype.RDF.LANGSTRING));

	}

	@Test
	public final void testCoreDatatypeTypedConstructorNullDatatype() {
		assertThrows(NullPointerException.class, () -> literal("label", ((CoreDatatype) null)));
	}

	//// String Value //////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testCoreDatatypeStringValue() {

		String label = "literal";
		String language = "en";
		CoreDatatype datatype = CoreDatatype.XSD.DECIMAL;

		assertThat(literal(label).stringValue()).isEqualTo(label);
		assertThat(literal(label, language).stringValue()).isEqualTo(label);
		assertThat(literal(label, datatype).stringValue()).isEqualTo(label);
	}

	//// Object Values /////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testCoreDatatypeBooleanValue() {

		CoreDatatype datatype = CoreDatatype.XSD.BOOLEAN;

		assertThat(literal("true", datatype).booleanValue()).isTrue();
		assertThat(literal("false", datatype).booleanValue()).isFalse();

		assertThat(literal("1", datatype).booleanValue()).isTrue();
		assertThat(literal("0", datatype).booleanValue()).isFalse();

		assertThat(literal("\ttrue", datatype).booleanValue()).isTrue();
		assertThat(literal("false\t", datatype).booleanValue()).isFalse();

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).booleanValue());

	}

	@Test
	public final void testCoreDatatypeByteValue() {

		CoreDatatype datatype = CoreDatatype.XSD.BYTE;
		Class<Byte> type = Byte.class;

		assertThat(literal("100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) 100);
		assertThat(literal("+100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) 100);
		assertThat(literal("-100", datatype).byteValue()).isInstanceOf(type).isEqualTo((byte) -100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).byteValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).booleanValue());

	}

	@Test
	public final void testCoreDatatypeShortValue() {

		CoreDatatype datatype = CoreDatatype.XSD.SHORT;
		Class<Short> type = Short.class;

		assertThat(literal("100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) 100);
		assertThat(literal("+100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) 100);
		assertThat(literal("-100", datatype).shortValue()).isInstanceOf(type).isEqualTo((short) -100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).shortValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).shortValue());

	}

	@Test
	public final void testCoreDatatypeIntValue() {

		CoreDatatype datatype = CoreDatatype.XSD.INT;
		Class<Integer> type = Integer.class;

		assertThat(literal("100", datatype).intValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).intValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).intValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).intValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).intValue());

	}

	@Test
	public final void testCoreDatatypeLongValue() {

		CoreDatatype datatype = CoreDatatype.XSD.LONG;
		Class<Long> type = Long.class;

		assertThat(literal("100", datatype).longValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).longValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).longValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).longValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).longValue());

	}

	@Test
	public final void testCoreDatatypeFloatValue() {

		CoreDatatype datatype = CoreDatatype.XSD.FLOAT;
		Class<Float> type = Float.class;

		assertThat(literal("100", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).floatValue()).isInstanceOf(type).isEqualTo(-100);
		assertThat(literal("100.0", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("10e1", datatype).floatValue()).isInstanceOf(type).isEqualTo(100);

		assertThat(literal("INF", datatype).floatValue()).isInstanceOf(type).isEqualTo(Float.POSITIVE_INFINITY);
		assertThat(literal("-INF", datatype).floatValue()).isInstanceOf(type).isEqualTo(Float.NEGATIVE_INFINITY);
		assertTrue(Float.isNaN(literal("NaN", datatype).floatValue()));

		// assertThatIllegalArgumentException().as("not normalized")
		// .isThrownBy(() -> literal("\t100", datatype).floatValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).floatValue());

	}

	@Test
	public final void testCoreDatatypeDoubleValue() {

		CoreDatatype datatype = CoreDatatype.XSD.DOUBLE;
		Class<Double> type = Double.class;

		assertThat(literal("100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).doubleValue()).isInstanceOf(type).isEqualTo(-100);
		assertThat(literal("100.0", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("10e1", datatype).doubleValue()).isInstanceOf(type).isEqualTo(100);

		assertThat(literal("INF", datatype).doubleValue()).isInstanceOf(type).isEqualTo(Double.POSITIVE_INFINITY);
		assertThat(literal("-INF", datatype).doubleValue()).isInstanceOf(type).isEqualTo(Double.NEGATIVE_INFINITY);
		assertTrue(Double.isNaN(literal("NaN", datatype).doubleValue()));

		// assertThatIllegalArgumentException().as("not normalized")
		// .isThrownBy(() -> literal("\t100", datatype).doubleValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).doubleValue());

	}

	@Test
	public final void testCoreDatatypeIntegerValue() {

		CoreDatatype datatype = CoreDatatype.XSD.INTEGER;
		Class<BigInteger> type = BigInteger.class;

		assertThat(literal("100", datatype).integerValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("+100", datatype).integerValue()).isInstanceOf(type).isEqualTo(100);
		assertThat(literal("-100", datatype).integerValue()).isInstanceOf(type).isEqualTo(-100);

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).integerValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).integerValue());

	}

	@Test
	public final void testCoreDatatypeDecimalValue() {

		CoreDatatype datatype = CoreDatatype.XSD.DECIMAL;
		Class<BigDecimal> type = BigDecimal.class;

		assertThat(literal("100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100"));
		assertThat(literal("+100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100"));
		assertThat(literal("-100", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("-100"));
		assertThat(literal("100.0", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("100.0"));
		assertThat(literal("10e1", datatype).decimalValue()).isInstanceOf(type).isEqualTo(new BigDecimal("1.0e2"));

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", datatype).decimalValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", datatype).decimalValue());

	}

	@Test
	public final void testCoreDatatypeTemporalDateTimeValue() {

		String integral = "2020-09-29T01:02:03";
		String fractional = "2020-09-29T01:02:03.004";

		String offset = "2020-09-29T01:02:03+05:00";
		String zero = "2020-09-29T01:02:03Z";

		assertThat(LocalDateTime.from(literal(integral, CoreDatatype.XSD.DATETIME).temporalAccessorValue()))
				.isEqualTo(LocalDateTime.parse(integral));

		assertThat(LocalDateTime.from(literal(fractional, CoreDatatype.XSD.DATETIME).temporalAccessorValue()))
				.isEqualTo(LocalDateTime.parse(fractional));

		assertThat(OffsetDateTime.from(literal(offset, CoreDatatype.XSD.DATETIME).temporalAccessorValue()))
				.isEqualTo(OffsetDateTime.parse(offset));

		assertThat(OffsetDateTime.from(literal(zero, CoreDatatype.XSD.DATETIME).temporalAccessorValue()))
				.isEqualTo(OffsetDateTime.parse(zero));

		Stream.of(

				"0001-01-01T00:00:00",
				"0001-01-01T00:00:00.0",
				"0001-01-01T00:00:00Z",
				"0001-01-01T00:00:00.0Z",
				"0001-01-01T00:00:00+00:00",
				"0001-01-01T00:00:00.0+00:00",
				"0001-01-01T00:00:00.0-00:00",
				"0001-01-01T00:00:00.0+14:00",
				"0001-01-01T00:00:00.0-14:00",
				"0001-05-31T00:00:00.00",
				"0001-07-31T00:00:00.00",
				"0001-08-31T00:00:00.00",
				"0001-10-31T00:00:00.00",
				"0001-12-31T00:00:00.00",
				"-0001-01-01T00:00:00",
				"1234-12-31T23:59:59",
				"1234-12-31T24:00:00",
				// "12345-12-31T24:00:00",
				// "1234-12-31T24:00:00.1234567890",
				"2004-02-29T00:00:00"

		)
				.forEach(
						value -> assertThatCode(() -> literal(value, CoreDatatype.XSD.DATETIME).temporalAccessorValue())
								.as(value)
								.doesNotThrowAnyException()
				);

	}

	@Test
	public final void testCoreDatatypeTemporalTimeValue() {

		String integral = "01:02:03";
		String fractional = "01:02:03.004";

		String offset = "01:02:03+05:00";
		String zero = "01:02:03Z";

		assertThat(LocalTime.from(literal(integral, CoreDatatype.XSD.TIME).temporalAccessorValue()))
				.isEqualTo(LocalTime.parse(integral));

		assertThat(LocalTime.from(literal(fractional, CoreDatatype.XSD.TIME).temporalAccessorValue()))
				.isEqualTo(LocalTime.parse(fractional));

		assertThat(OffsetTime.from(literal(offset, CoreDatatype.XSD.TIME).temporalAccessorValue()))
				.isEqualTo(OffsetTime.parse(offset));

		assertThat(OffsetTime.from(literal(zero, CoreDatatype.XSD.TIME).temporalAccessorValue()))
				.isEqualTo(OffsetTime.parse(zero));
	}

	@Test
	public final void testCoreDatatypeTemporalDateValue() {

		String local = "2020-11-14";
		String offset = "2020-11-14+05:00";
		String zero = "2020-11-14Z";

		assertThat(LocalDate.from(literal(local, CoreDatatype.XSD.DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(local));

		assertThat(LocalDate.from(literal(offset, CoreDatatype.XSD.DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(offset.substring(0, 10))); // OffsetDate not supported by java.time

		assertThat(LocalDate.from(literal(zero, CoreDatatype.XSD.DATE).temporalAccessorValue()))
				.isEqualTo(LocalDate.parse(offset.substring(0, 10))); // OffsetDate not supported by java.time

	}

	@Test
	public final void testCoreDatatypeTemporalGYearMonthValue() {

		String base = "2020-11";

		assertThat(YearMonth.from(literal(base, CoreDatatype.XSD.GYEARMONTH).temporalAccessorValue()))
				.isEqualTo(YearMonth.parse(base));

	}

	@Test
	public final void testCoreDatatypeTemporalGYearValue() {

		String local = "2020";

		assertThat(Year.from(literal(local, CoreDatatype.XSD.GYEAR).temporalAccessorValue()))
				.isEqualTo(Year.parse(local));

	}

	@Test
	public final void testCoreDatatypeTemporalGMonthDayValue() {

		String local = "--11-14";

		assertThat(MonthDay.from(literal(local, CoreDatatype.XSD.GMONTHDAY).temporalAccessorValue()))
				.isEqualTo(MonthDay.parse(local));

	}

	@Test
	public final void testCoreDatatypeTemporalGDayValue() {

		String local = "---14";

		assertThat(literal(local, CoreDatatype.XSD.GDAY).temporalAccessorValue().get(ChronoField.DAY_OF_MONTH))
				.isEqualTo(14);

	}

	@Test
	public final void testCoreDatatypeTemporalGMonthValue() {

		String local = "--11";

		assertThat(Month.from(literal(local, CoreDatatype.XSD.GMONTH).temporalAccessorValue()))
				.isEqualTo(Month.NOVEMBER);

	}

	@Test
	public final void testCoreDatatypeTemporalAccessorMalformedValue() {

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("", CoreDatatype.XSD.DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("--", CoreDatatype.XSD.DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("2020-11-16T", CoreDatatype.XSD.DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("2020-11-16T11:12:13.", CoreDatatype.XSD.DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("malformed", CoreDatatype.XSD.DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("2020-11-16T", CoreDatatype.XSD.DATETIME).temporalAccessorValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("2020-11-16T11:12:13.", CoreDatatype.XSD.DATETIME).temporalAccessorValue());

		Stream.of(

				"foo", "Mon, 11 Jul 2005 09:22:29 +0200",
				"0001-01-01T00:00",
				"0001-01-01T00:00.00",
				"0001-13-01T00:00:00.00",
				"0001-01-32T00:00:00.00",
				// "0001-02-30T00:00:00.00",
				// "2005-02-29T00:00:00", // not a leap year
				// "0001-04-31T00:00:00.00",
				"0001-01-01T25:00:00.00",
				"0001-01-01T00:61:00.00",
				"0001-01-01T00:00:61.00",
				"0001-01-01T00:00.00+15:00",
				"0001-01-01T00:00.00-15:00",
				"001-01-01T00:00:00.0",
				"0001-1-01T00:00:00.0",
				"0001-01-1T00:00:00.0",
				"0001-01-01T0:00:00.0",
				"0001-01-01T00:0:00.0",
				"0001-01-01T00:00:0.0",
				"0001/01-01T00:00:00.0",
				"0001-01/01T00:00:00.0",
				"0001-01-01t00:00:00.0",
				"0001-01-01T00.00:00.0",
				"0001-01-01T00:00.00.0",
				"0001-01-01T00:00:00:0",
				"0001-01-01T00:00.00+0:00",
				"0001-01-01T00:00.00+00:0",
				"0001-jan-01T00:00:00",
				"0001-01-01T00:00:00+00:00Z",
				"0001-01-01T24:01:00", "0001-01-01T24:00:01",
				"00001-01-01T00:00:00",
				"0001-001-01T00:00:00",
				"0001-01-001T00:00:00",
				"0001-01-01T000:00:00",
				"0001-01-01T00:000:00",
				"0001-01-01T00:00:000",
				"0001-01-01T00:00:000",
				"0001-01-01T00:00:00z",
				"0001-01-01T00:00:00+05",
				"0001-01-01T00:00:00+0500",
				"0001-01-01T00:00:00GMT",
				"0001-01-01T00:00:00PST",
				"0001-01-01T00:00:00GMT+05",
				// "0000-01-01T00:00:00",
				"-0000-01-01T00:00:00",
				"+0001-01-01T00:00:00"

		)
				.forEach(value -> assertThatExceptionOfType(DateTimeException.class)
						.as(value)
						.isThrownBy(() -> literal(value, CoreDatatype.XSD.DATETIME).temporalAccessorValue())
				);

	}

	@Test
	public final void testCoreDatatypeTemporalDurationValue() {

		String period = "P1Y2M3D";
		String duration = "PT1H2M3.4S";

		assertThat(Period.from(literal(period, CoreDatatype.XSD.DURATION).temporalAmountValue()))
				.isEqualTo(Period.parse(period));

		assertThat(Period.from(literal("-P1Y2M3D", CoreDatatype.XSD.DURATION).temporalAmountValue()))
				.isEqualTo(Period.parse(period).negated());

		assertThat(Duration.from(literal(duration, CoreDatatype.XSD.DURATION).temporalAmountValue()))
				.isEqualTo(Duration.parse(duration));

	}

	@Test
	public final void testCoreDatatypeTemporalAmountMalformedValue() {

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("", CoreDatatype.XSD.DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class)
				.isThrownBy(() -> literal("malformed", CoreDatatype.XSD.DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no  components")
				.isThrownBy(() -> literal("P", CoreDatatype.XSD.DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time components")
				.isThrownBy(() -> literal("P1Y2MT", CoreDatatype.XSD.DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("negative component")
				.isThrownBy(() -> literal("P-1347M ", CoreDatatype.XSD.DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("no time separator")
				.isThrownBy(() -> literal("P1Y1S ", CoreDatatype.XSD.DURATION).temporalAmountValue());

		assertThatExceptionOfType(DateTimeException.class).as("missing fractional digits after dot")
				.isThrownBy(() -> literal("PT1.S", CoreDatatype.XSD.DURATION).temporalAmountValue());
	}

	@Test
	public final void testCoreDatatypeCalendarValue() throws DatatypeConfigurationException {

		Class<XMLGregorianCalendar> type = XMLGregorianCalendar.class;

		DatatypeFactory factory = DatatypeFactory.newInstance();

		Function<Consumer<XMLGregorianCalendar>, XMLGregorianCalendar> setup = consumer -> {

			XMLGregorianCalendar calendar = factory.newXMLGregorianCalendar();

			consumer.accept(calendar);

			return calendar;

		};

		assertThat(literal("2020-09-29T01:02:03.004Z", CoreDatatype.XSD.DATETIME).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
					calendar.setDay(29);
					calendar.setTime(1, 2, 3, 4);
					calendar.setTimezone(0);
				}));

		assertThat(literal("01:02:03.004", CoreDatatype.XSD.TIME).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setTime(1, 2, 3, 4);
				}));

		assertThat(literal("2020-09-29", CoreDatatype.XSD.DATE).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
					calendar.setDay(29);
				}));

		assertThat(literal("2020-09", CoreDatatype.XSD.GYEARMONTH).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
					calendar.setMonth(9);
				}));

		assertThat(literal("--09-29", CoreDatatype.XSD.GMONTHDAY).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setMonth(9);
					calendar.setDay(29);
				}));

		assertThat(literal("2020", CoreDatatype.XSD.GYEAR).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setYear(2020);
				}));

		assertThat(literal("--09", CoreDatatype.XSD.GMONTH).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setMonth(9);
				}));

		assertThat(literal("---29", CoreDatatype.XSD.GDAY).calendarValue())
				.isInstanceOf(type)
				.isEqualTo(setup.apply(calendar -> {
					calendar.setDay(29);
				}));

		assertThatIllegalArgumentException().as("not normalized")
				.isThrownBy(() -> literal("\t100", CoreDatatype.XSD.DATETIME).calendarValue());

		assertThatIllegalArgumentException().as("malformed")
				.isThrownBy(() -> literal("malformed", CoreDatatype.XSD.DATETIME).calendarValue());

	}

	////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	@Test
	public void testCoreDatatypeEqualsAndHashCode() {

		Literal plain = literal("plain");
		Literal tagged = literal("tagged", "en");
		Literal typed = literal("typed", datatype("http://example.org/datatype"));

		Literal _plain = literal(plain.getLabel());
		Literal _tagged = literal(tagged.getLabel(), tagged.getLanguage().orElse(""));
		Literal _typed = literal(typed.getLabel(), typed.getDatatype());

		assertThat(plain).isEqualTo(plain);
		assertThat(plain).isEqualTo(_plain);

		assertThat(tagged).isEqualTo(tagged);
		assertThat(tagged).isEqualTo(_tagged);

		assertThat(typed).isEqualTo(typed);
		assertThat(typed).isEqualTo(_typed);

		assertThat(plain).isNotEqualTo(null);
		assertThat(plain).isNotEqualTo(new Object());

		assertThat(plain).isNotEqualTo(tagged);
		assertThat(plain).isNotEqualTo(typed);
		assertThat(tagged).isNotEqualTo(typed);

		assertThat(plain).isNotEqualTo(literal("other"));
		assertThat(tagged).isNotEqualTo(literal(tagged.getLabel(), "other"));
		assertThat(typed).isNotEqualTo(literal(typed.getLabel(), "http://example.org/other"));

		// hashCode() should return identical values for literals for which equals() is true

		assertThat(plain.hashCode()).isEqualTo(_plain.hashCode());
		assertThat(tagged.hashCode()).isEqualTo(_tagged.hashCode());
		assertThat(typed.hashCode()).isEqualTo(_typed.hashCode());

		assertThat(tagged.hashCode())
				.as("computed according to contract")
				.isEqualTo(tagged.getLabel().hashCode()); // !!! label >> label+language+datatype

	}

	@Test
	public final void testCoreDatatypeEqualsAndHashCodeCaseInsensitiveLanguage() {

		Literal lowercase = literal("label", "en");
		Literal uppercase = literal("label", "EN");

		assertThat(lowercase).isEqualTo(uppercase);
		assertThat(lowercase.hashCode()).isEqualTo(uppercase.hashCode());
	}

	@Test
	public final void testCoreDatatypeEqualsAndHashCodeXSDString() {

		// in RDF 1.1, there is no distinction between plain and string-typed literals

		Literal plain = literal("label");
		Literal typed = literal("label", CoreDatatype.XSD.STRING);

		assertThat(plain).isEqualTo(typed);
		assertThat(plain.hashCode()).isEqualTo(typed.hashCode());
	}

	@Test
	public final void testSerializationWithCoreDatatypeXsd() {
		Literal literal = literal("1", datatype(XSD_INT));

		byte[] bytes = objectToBytes(literal);
		Literal roundTrip = (Literal) bytesToObject(bytes);

		assertEquals(CoreDatatype.XSD.INT, roundTrip.getCoreDatatype());
	}

	@Test
	public final void testSerializationWithCoreDatatypeRdfLangString() {
		Literal literal = literal("hei", "no");
		assertEquals(CoreDatatype.RDF.LANGSTRING, literal.getCoreDatatype());

		byte[] bytes = objectToBytes(literal);
		Literal roundTrip = (Literal) bytesToObject(bytes);

		assertEquals(CoreDatatype.RDF.LANGSTRING, roundTrip.getCoreDatatype());
	}

	@Test
	public final void testSerializationWithCoreDatatypeGEO() {
		Literal literal = literal("1", CoreDatatype.GEO.WKT_LITERAL);

		byte[] bytes = objectToBytes(literal);
		Literal roundTrip = (Literal) bytesToObject(bytes);

		assertEquals(CoreDatatype.GEO.WKT_LITERAL, roundTrip.getCoreDatatype());
	}

	@Test
	public final void testSerializationWithCoreDatatype4() {
		Literal literal = literal("1", datatype("http://example.org/dt1"));
		assertEquals(CoreDatatype.XSD.NONE, literal.getCoreDatatype());

		byte[] bytes = objectToBytes(literal);
		Literal roundTrip = (Literal) bytesToObject(bytes);

		assertEquals(CoreDatatype.XSD.NONE, roundTrip.getCoreDatatype());
	}

	private byte[] objectToBytes(Serializable object) {
		try (var byteArrayOutputStream = new ByteArrayOutputStream()) {
			try (var objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
				objectOutputStream.writeObject(object);
			}
			return byteArrayOutputStream.toByteArray();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private Object bytesToObject(byte[] str) {
		try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(str)) {
			try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
				return objectInputStream.readObject();
			}
		} catch (IOException | ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}

}