HTTPRepository.java

/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 *
 * 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.http;

import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.client.HttpClient;
import org.eclipse.rdf4j.http.client.HttpClientDependent;
import org.eclipse.rdf4j.http.client.HttpClientSessionManager;
import org.eclipse.rdf4j.http.client.RDF4JProtocolSession;
import org.eclipse.rdf4j.http.client.SPARQLProtocolSession;
import org.eclipse.rdf4j.http.client.SessionManagerDependent;
import org.eclipse.rdf4j.http.client.SharedHttpClientSessionManager;
import org.eclipse.rdf4j.http.protocol.Protocol;
import org.eclipse.rdf4j.http.protocol.UnauthorizedException;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.util.Literals;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.QueryEvaluationException;
import org.eclipse.rdf4j.query.TupleQueryResult;
import org.eclipse.rdf4j.query.resultio.TupleQueryResultFormat;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.base.AbstractRepository;
import org.eclipse.rdf4j.rio.RDFFormat;

/**
 * A repository that serves as a client for a remote repository on an RDF4J Server. Methods in this class may throw the
 * specific {@link RepositoryException} subclass {@link UnauthorizedException}, the semantics of which is defined by the
 * HTTP protocol.
 * <p>
 * This repository client uses a <a href="https://rdf4j.org/documentation/reference/rest-api/">RDF4J-specific extension
 * of the SPARQL 1.1 Protocol</a> to communicate with the server. For communicating with a SPARQL endpoint that is not
 * based on RDF4J, it is recommended to use {@link org.eclipse.rdf4j.repository.sparql.SPARQLRepository
 * SPARQLRepository} instead.
 *
 * @author Arjohn Kampman
 * @author Jeen Broekstra
 * @author Herko ter Horst
 * @see org.eclipse.rdf4j.http.protocol.UnauthorizedException
 */
public class HTTPRepository extends AbstractRepository implements HttpClientDependent, SessionManagerDependent {

	/*-----------*
	 * Variables *
	 *-----------*/

	/**
	 * The HTTP client that takes care of the client-server communication.
	 */
	private volatile HttpClientSessionManager sessionManager;

	/**
	 * dependent life cycle
	 */
	private volatile SharedHttpClientSessionManager dependentSessionManager;

	private String username;

	private String password;

	private String serverURL;

	private String repositoryURL;

	private RDFFormat rdfFormat;

	private TupleQueryResultFormat tupleFormat;

	private File dataDir;

	private volatile Boolean compatibleMode = null;

	private volatile Map<String, String> additionalHttpHeaders = Collections.emptyMap();

	private HTTPRepository() {
		super();
	}

	public HTTPRepository(final String serverURL, final String repositoryID) {
		this();
		this.serverURL = serverURL;
		this.repositoryURL = Protocol.getRepositoryLocation(serverURL, repositoryID);
	}

	public HTTPRepository(final String repositoryURL) {
		this();
		// Try to parse the server URL from the repository URL
		Pattern urlPattern = Pattern.compile("(.*)/" + Protocol.REPOSITORIES + "/[^/]*/?");
		Matcher matcher = urlPattern.matcher(repositoryURL);

		if (matcher.matches() && matcher.groupCount() == 1) {
			this.serverURL = matcher.group(1);
		} else {
			throw new IllegalArgumentException("URL must be to a RDF4J Repository (not just the server)");
		}
		this.repositoryURL = repositoryURL;
	}

	@Override
	public void setDataDir(final File dataDir) {
		this.dataDir = dataDir;
	}

	@Override
	public File getDataDir() {
		return dataDir;
	}

	@Override
	public HttpClientSessionManager getHttpClientSessionManager() {
		HttpClientSessionManager result = sessionManager;
		if (result == null) {
			synchronized (this) {
				result = sessionManager;
				if (result == null) {
					result = sessionManager = dependentSessionManager = new SharedHttpClientSessionManager();
				}
			}
		}
		return result;
	}

	@Override
	public void setHttpClientSessionManager(HttpClientSessionManager client) {
		synchronized (this) {
			this.sessionManager = client;
			// If they set a client, we need to check whether we need to
			// shutdown any existing dependentClient
			SharedHttpClientSessionManager toCloseDependentClient = dependentSessionManager;
			dependentSessionManager = null;
			if (toCloseDependentClient != null) {
				toCloseDependentClient.shutDown();
			}
		}
	}

	/**
	 * Get the additional HTTP headers which will be used
	 *
	 * @return a read-only view of the additional HTTP headers which will be included in every request to the server.
	 */
	public Map<String, String> getAdditionalHttpHeaders() {
		return Collections.unmodifiableMap(additionalHttpHeaders);
	}

	/**
	 * Set additional HTTP headers to be included in every request to the server, which may be required for certain
	 * unusual server configurations. This will only take effect on connections subsequently returned by
	 * {@link #getConnection()}.
	 *
	 * @param additionalHttpHeaders a map containing pairs of header names and values. May be null
	 */
	public void setAdditionalHttpHeaders(Map<String, String> additionalHttpHeaders) {
		if (additionalHttpHeaders == null) {
			this.additionalHttpHeaders = Collections.emptyMap();
		} else {
			this.additionalHttpHeaders = additionalHttpHeaders;
		}
	}

	@Override
	public final HttpClient getHttpClient() {
		return getHttpClientSessionManager().getHttpClient();
	}

	@Override
	public void setHttpClient(HttpClient httpClient) {
		SharedHttpClientSessionManager toSetDependentSessionManager = dependentSessionManager;
		if (toSetDependentSessionManager == null) {
			getHttpClientSessionManager();
			toSetDependentSessionManager = dependentSessionManager;
		}
		// The strange lifecycle results in the possibility that the
		// dependentSessionManger will be null due to a call to setHttpClient, so add
		// a null guard here for that possibility
		if (toSetDependentSessionManager != null) {
			toSetDependentSessionManager.setHttpClient(httpClient);
		}
	}

	@Override
	public ValueFactory getValueFactory() {
		return SimpleValueFactory.getInstance();
	}

	@Override
	public RepositoryConnection getConnection() throws RepositoryException {
		if (!isInitialized()) {
			init();
		}
		return new HTTPRepositoryConnection(this, createHTTPClient());
	}

	@Override
	public boolean isWritable() throws RepositoryException {
		if (!isInitialized()) {
			init();
		}

		boolean isWritable = false;

		try (RDF4JProtocolSession client = createHTTPClient()) {
			final String repositoryURL = client.getRepositoryURL();
			try (TupleQueryResult repositoryList = client.getRepositoryList()) {
				while (repositoryList.hasNext()) {
					final BindingSet bindingSet = repositoryList.next();
					final Value uri = bindingSet.getValue("uri");

					if (uri != null && uri.stringValue().equals(repositoryURL)) {
						isWritable = Literals.getBooleanValue(bindingSet.getValue("writable"), false);
						break;
					}
				}
			}
		} catch (QueryEvaluationException | IOException e) {
			throw new RepositoryException(e);
		}

		return isWritable;
	}

	/**
	 * Sets the preferred serialization format for tuple query results to the supplied {@link TupleQueryResultFormat},
	 * overriding the {@link SPARQLProtocolSession} 's default preference. Setting this parameter is not necessary in
	 * most cases as the {@link SPARQLProtocolSession} by default indicates a preference for the most compact and
	 * efficient format available.
	 *
	 * @param format the preferred {@link TupleQueryResultFormat}. If set to 'null' no explicit preference will be
	 *               stated.
	 */
	public void setPreferredTupleQueryResultFormat(final TupleQueryResultFormat format) {
		this.tupleFormat = format;
	}

	/**
	 * Indicates the current preferred {@link TupleQueryResultFormat}.
	 *
	 * @return The preferred format, of 'null' if no explicit preference is defined.
	 */
	public TupleQueryResultFormat getPreferredTupleQueryResultFormat() {
		return tupleFormat;
	}

	/**
	 * Sets the preferred serialization format for RDF to the supplied {@link RDFFormat}, overriding the
	 * {@link SPARQLProtocolSession}'s default preference. Setting this parameter is not necessary in most cases as the
	 * {@link SPARQLProtocolSession} by default indicates a preference for the most compact and efficient format
	 * available.
	 * <p>
	 * Use with caution: if set to a format that does not support context serialization any context info contained in
	 * the query result will be lost.
	 *
	 * @param format the preferred {@link RDFFormat}. If set to 'null' no explicit preference will be stated.
	 */
	public void setPreferredRDFFormat(final RDFFormat format) {
		this.rdfFormat = format;
	}

	/**
	 * Indicates the current preferred {@link RDFFormat}.
	 *
	 * @return The preferred format, of 'null' if no explicit preference is defined.
	 */
	public RDFFormat getPreferredRDFFormat() {
		return rdfFormat;
	}

	/**
	 * Set the username and password to use for authenticating with the remote repository.
	 *
	 * @param username the username. Setting this to null will disable authentication.
	 * @param password the password. Setting this to null will disable authentication.
	 */
	public void setUsernameAndPassword(final String username, final String password) {
		this.username = username;
		this.password = password;
	}

	public String getRepositoryURL() {
		return repositoryURL;
	}

	/*
	 * -------------------* non-public methods * -------------------
	 */

	@Override
	protected void initializeInternal() throws RepositoryException {
		// no-op
	}

	@Override
	protected void shutDownInternal() throws RepositoryException {
		try {
			SharedHttpClientSessionManager toCloseDependentClient = dependentSessionManager;
			dependentSessionManager = null;
			if (toCloseDependentClient != null) {
				toCloseDependentClient.shutDown();
			}
		} finally {
			// remove reference but do not shut down, client may be shared by
			// other repos.
			sessionManager = null;
		}
	}

	/**
	 * Creates a new {@link RDF4JProtocolSession} object.
	 *
	 * @return a {@link RDF4JProtocolSession} object.
	 */
	protected RDF4JProtocolSession createHTTPClient() {
		// initialize HTTP client
		RDF4JProtocolSession httpClient = getHttpClientSessionManager().createRDF4JProtocolSession(serverURL);
		httpClient.setValueFactory(SimpleValueFactory.getInstance());
		if (repositoryURL != null) {
			httpClient.setRepository(repositoryURL);
		}
		if (tupleFormat != null) {
			httpClient.setPreferredTupleQueryResultFormat(tupleFormat);
		}
		if (rdfFormat != null) {
			httpClient.setPreferredRDFFormat(rdfFormat);
		}
		if (username != null) {
			httpClient.setUsernameAndPassword(username, password);
		}
		httpClient.setAdditionalHttpHeaders(additionalHttpHeaders);
		return httpClient;
	}

	/**
	 * Verify if transaction handling should be done in backward-compatible mode (this is the case when communicating
	 * with an older RDF4J Server).
	 *
	 * @return <code>true</code> if the Server does not support the extended transaction protocol, <code>false</code>
	 *         otherwise.
	 * @throws RepositoryException if something went wrong while querying the server for the protocol version.
	 */
	boolean useCompatibleMode() throws RepositoryException {
		Boolean result = compatibleMode;
		if (result == null) {
			synchronized (this) {
				result = compatibleMode;
				if (result == null) {
					// protocol version 7 supports the new transaction
					// handling. If the server is older, we need to run in
					// backward-compatible mode.
					result = compatibleMode = (getServerProtocolVersion() < 7);
				}
			}
		}
		return result;
	}

	/**
	 * Get the RDF4J Server's protocol version, as an integer
	 *
	 * @return the protocol version implemented by the RDF4J, as an integer number.
	 */
	int getServerProtocolVersion() {
		try (RDF4JProtocolSession client = createHTTPClient()) {
			final String serverProtocolVersion = client.getServerProtocol();
			return Integer.parseInt(serverProtocolVersion);
		} catch (NumberFormatException | IOException e) {
			throw new RepositoryException("could not read protocol version from server: ", e);
		}

	}
}