SPARQLConnectionTest.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.repository.sparql;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.rdf4j.model.util.Values.iri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.lang.ref.WeakReference;

import org.eclipse.rdf4j.http.client.SPARQLProtocolSession;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.query.impl.MapBindingSet;
import org.eclipse.rdf4j.query.impl.SimpleBinding;
import org.eclipse.rdf4j.query.impl.TupleQueryResultBuilder;
import org.eclipse.rdf4j.query.parser.ParsedQuery;
import org.eclipse.rdf4j.query.parser.sparql.SPARQLParser;
import org.eclipse.rdf4j.query.parser.sparql.SPARQLParserFactory;
import org.eclipse.rdf4j.rio.ParserConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;

public class SPARQLConnectionTest {

	private SPARQLConnection subject;
	private SPARQLProtocolSession client;
	private final ValueFactory vf = SimpleValueFactory.getInstance();

	@BeforeEach
	public void setUp() {
		client = mock(SPARQLProtocolSession.class);
		subject = new SPARQLConnection(null, client);
	}

	@Test
	public void setParserConfigPassesToProtocolSession() {
		ParserConfig config = new ParserConfig();

		subject.setParserConfig(config);
		verify(client, times(1)).setParserConfig(config);
	}

	@Test
	public void commitOnEmptyTxnDoesNothing() throws Exception {
		subject.begin();
		subject.commit();

		// verify both method signatures for sendUpdate never get called.
		verify(client, never()).sendUpdate(any(), any(), any(), any(), anyBoolean(), anyInt(), any());
		verify(client, never()).sendUpdate(any(), any(), any(), any(), anyBoolean(), any());
	}

	@Test
	public void testGroupingAddsInInsert() throws Exception {
		ArgumentCaptor<String> sparqlUpdateCaptor = ArgumentCaptor.forClass(String.class);

		subject.begin();
		subject.add(FOAF.PERSON, RDF.TYPE, RDFS.CLASS);
		subject.add(FOAF.AGENT, RDF.TYPE, RDFS.CLASS);
		subject.commit();

		verify(client).sendUpdate(any(), sparqlUpdateCaptor.capture(), any(), any(), anyBoolean(), anyInt(), any());

		String sparqlUpdate = sparqlUpdateCaptor.getValue();
		String expectedTriple1 = "<" + FOAF.PERSON + "> <" + RDF.TYPE + "> <" + RDFS.CLASS + ">";
		String expectedTriple2 = "<" + FOAF.AGENT + "> <" + RDF.TYPE + "> <" + RDFS.CLASS + ">";

		assertThat(sparqlUpdate).containsOnlyOnce("INSERT DATA").contains(expectedTriple1).contains(expectedTriple2);
	}

	@Test
	public void testAddSingleContextHandling() throws Exception {
		ArgumentCaptor<String> sparqlUpdateCaptor = ArgumentCaptor.forClass(String.class);

		IRI g1 = vf.createIRI("urn:g1");

		subject.begin();
		subject.add(FOAF.PERSON, RDF.TYPE, RDFS.CLASS, g1);
		subject.remove(FOAF.AGENT, RDF.TYPE, RDFS.CLASS);
		subject.commit();

		verify(client).sendUpdate(any(), sparqlUpdateCaptor.capture(), any(), any(), anyBoolean(), anyInt(), any());

		String sparqlUpdate = sparqlUpdateCaptor.getValue();
		String expectedAddPattern = "INSERT DATA[^{]*\\{[^G]*GRAPH <" + g1 + ">[^{]*\\{[^<]*<" + FOAF.PERSON + "> ";
		String expectedRemovePattern = "DELETE DATA[^{]*\\{[^<]*<" + FOAF.AGENT + "> ";

		assertThat(sparqlUpdate).containsPattern(expectedAddPattern).containsPattern(expectedRemovePattern);
	}

	@Test
	public void testSizeQuery() throws Exception {

		String sizeAsTupleQuery = subject.sizeAsTupleQuery();
		ParsedQuery query = new SPARQLParserFactory().getParser().parseQuery(sizeAsTupleQuery, "http://example.org/");
		assertNotNull(query);

		sizeAsTupleQuery = subject.sizeAsTupleQuery(vf.createIRI("urn:g1"));
		query = new SPARQLParserFactory().getParser().parseQuery(sizeAsTupleQuery, "http://example.org/");
		assertNotNull(query);

		sizeAsTupleQuery = subject.sizeAsTupleQuery(vf.createIRI("urn:g1"), vf.createIRI("urn:g2"));
		query = new SPARQLParserFactory().getParser().parseQuery(sizeAsTupleQuery, "http://example.org/");
		assertNotNull(query);

		sizeAsTupleQuery = subject.sizeAsTupleQuery(vf.createIRI("urn:g1"), vf.createBNode());
		query = new SPARQLParserFactory().getParser().parseQuery(sizeAsTupleQuery, "http://example.org/");
		assertNotNull(query);

		sizeAsTupleQuery = subject.sizeAsTupleQuery(RDF4J.NIL);
		query = new SPARQLParserFactory().getParser().parseQuery(sizeAsTupleQuery, "http://example.org/");
		assertNotNull(query);
		assertFalse(sizeAsTupleQuery.contains("nil"));

		sizeAsTupleQuery = subject.sizeAsTupleQuery(null);
		query = new SPARQLParserFactory().getParser().parseQuery(sizeAsTupleQuery, "http://example.org/");

		assertNotNull(query);
	}

	@Test
	public void testAddMultipleContextHandling() throws Exception {
		ArgumentCaptor<String> sparqlUpdateCaptor = ArgumentCaptor.forClass(String.class);

		IRI g1 = vf.createIRI("urn:g1");
		IRI g2 = vf.createIRI("urn:g2");

		subject.begin();
		subject.add(FOAF.PERSON, RDF.TYPE, RDFS.CLASS, g1, g2);
		subject.remove(FOAF.AGENT, RDF.TYPE, RDFS.CLASS);
		subject.commit();

		verify(client).sendUpdate(any(), sparqlUpdateCaptor.capture(), any(), any(), anyBoolean(), anyInt(), any());

		String sparqlUpdate = sparqlUpdateCaptor.getValue();
		String expectedAddPattern1 = "INSERT DATA[^{]*\\{[^G]*GRAPH <" + g1 + ">[^{]*\\{[^<]*<" + FOAF.PERSON + "> ";
		String expectedAddPattern2 = "INSERT DATA[^{]*\\{[^G]*GRAPH <" + g2 + ">[^{]*\\{[^<]*<" + FOAF.PERSON + "> ";
		String expectedRemovePattern = "DELETE DATA[^{]*\\{[^<]*<" + FOAF.AGENT + "> ";

		assertThat(sparqlUpdate).containsPattern(expectedAddPattern1)
				.containsPattern(expectedAddPattern2)
				.containsPattern(expectedRemovePattern);
	}

	@Test
	public void testHandlingAddsRemoves() throws Exception {
		ArgumentCaptor<String> sparqlUpdateCaptor = ArgumentCaptor.forClass(String.class);

		subject.begin();
		subject.add(FOAF.PERSON, RDF.TYPE, RDFS.CLASS);
		subject.add(FOAF.AGENT, RDF.TYPE, RDFS.CLASS);
		subject.remove(FOAF.BIRTHDAY, RDF.TYPE, RDF.PROPERTY);
		subject.add(FOAF.AGE, RDF.TYPE, RDF.PROPERTY);
		subject.commit();

		verify(client).sendUpdate(any(), sparqlUpdateCaptor.capture(), any(), any(), anyBoolean(), anyInt(), any());

		String sparqlUpdate = sparqlUpdateCaptor.getValue();

		String expectedAddedTriple1 = "<" + FOAF.PERSON + "> <" + RDF.TYPE + "> <" + RDFS.CLASS + "> .";
		String expectedAddedTriple2 = "<" + FOAF.AGENT + "> <" + RDF.TYPE + "> <" + RDFS.CLASS + "> .";
		String expectedAddedTriple3 = "<" + FOAF.AGE + "> <" + RDF.TYPE + "> <" + RDF.PROPERTY + "> ";
		String expectedRemovedTriple1 = "<" + FOAF.BIRTHDAY + "> <" + RDF.TYPE + "> <" + RDF.PROPERTY + "> .";

		String expectedSequence = "INSERT DATA[^{]*\\{[^}]*\\}[^D]+DELETE DATA[^{]*\\{[^}]*\\}[^I]+INSERT DATA.*";

		assertThat(sparqlUpdate).containsPattern(expectedSequence);
		assertThat(sparqlUpdate).contains(expectedAddedTriple1)
				.contains(expectedAddedTriple2)
				.contains(expectedAddedTriple3)
				.contains(expectedRemovedTriple1);

	}

	@Test
	public void testSilentClear() throws Exception {
		subject.setSilentClear(true);
		assertThat(subject.isSilentClear());

		ArgumentCaptor<String> sparqlUpdateCaptor = ArgumentCaptor.forClass(String.class);
		subject.begin();
		subject.clear();
		subject.commit();

		verify(client).sendUpdate(any(), sparqlUpdateCaptor.capture(), any(), any(), anyBoolean(), anyInt(), any());

		String sparqlUpdate = sparqlUpdateCaptor.getValue();
		assertThat(sparqlUpdate).containsOnlyOnce("CLEAR SILENT");
	}

	@Test
	public void testSilentClear_NamedGraph() throws Exception {
		subject.setSilentClear(true);
		assertThat(subject.isSilentClear());

		ArgumentCaptor<String> sparqlUpdateCaptor = ArgumentCaptor.forClass(String.class);
		subject.begin();
		subject.clear(iri("http://example.org/"));
		subject.commit();

		verify(client).sendUpdate(any(), sparqlUpdateCaptor.capture(), any(), any(), anyBoolean(), anyInt(), any());

		String sparqlUpdate = sparqlUpdateCaptor.getValue();
		assertThat(sparqlUpdate).containsOnlyOnce("CLEAR SILENT GRAPH <http://example.org/>");
	}
}