Repository: Wikidata/Wikidata-Toolkit
Pull request #435 (2019-08-29 12:01:52)

Two small fixes for PropertyRegister API fetcher
This PR fixes two small issues with the current property type fetching code:

* we now fetch the URI pattern for ExternalIDs as well, previously we only tried to fetch it if the property has type String.
* we keep track of properties that are missing, in order to avoid unnecessary requests

The second point is especially relevant if combined with #434, since without that fix a single missing property can potentially cause a lot of API requests to be made (all properties below the missing ID will need to be fetched until `smallestUnfetchedPropertyIdNumber` is large enough to include the missing property id)


Patch:
diff --git a/pom.xml b/pom.xml
index 58023235..250e4493 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,6 +22,7 @@
 		<module>wdtk-examples</module>
 		<module>wdtk-distribution</module>
 		<module>wdtk-rdf</module>
+		<module>wdtk-client</module>
 	</modules>
 
 	<licenses>
diff --git a/wdtk-client/LICENSE.txt b/wdtk-client/LICENSE.txt
new file mode 100644
index 00000000..261eeb9e
--- /dev/null
+++ b/wdtk-client/LICENSE.txt
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/wdtk-client/conversion.ini b/wdtk-client/conversion.ini
new file mode 100644
index 00000000..7cf6d857
--- /dev/null
+++ b/wdtk-client/conversion.ini
@@ -0,0 +1,41 @@
+[general]
+offline = true
+[json-dump]
+format = json
+destination = WikidataDump.json
+compression = .gz
+[rdf-all_exact_data]
+format = rdf
+destination = wikidata-properties.nt
+compression = .gz
+rdfdump = ALL_EXACT_DATA
+[rdf-terms]
+format = rdf
+destination = wikidata-terms.nt
+compression = .gz
+rdfdump = TERMS
+[rdf-statements]
+format = rdf
+destination = wikidata-statements.nt
+compression = .gz
+rdfdump = STATEMENTS
+[rdf-simple_statements]
+format = rdf
+destination = wikidata-simple-statements.nt
+compression = .gz
+rdfdump = SIMPLE_STATEMENTS
+[rdf-taxonomy]
+format = rdf
+destination = wikidata-taxonomy.nt
+compression = .gz
+rdfdump = TAXONOMY
+[rdf-instance_of]
+format = rdf
+destination = wikidata-instances.nt
+compression = .gz
+rdfdump = INSTANCE_OF
+[rdf-sitelinks]
+format = rdf
+destination = wikidata-sitelinks.nt
+compression = .gz
+rdfdump = SITELINKS
diff --git a/wdtk-client/pom.xml b/wdtk-client/pom.xml
new file mode 100644
index 00000000..6737f105
--- /dev/null
+++ b/wdtk-client/pom.xml
@@ -0,0 +1,67 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>org.wikidata.wdtk</groupId>
+		<artifactId>wdtk-parent</artifactId>
+		<version>0.11.0-SNAPSHOT</version>
+	</parent>
+
+	<artifactId>wdtk-client</artifactId>
+	<packaging>jar</packaging>
+
+	<name>Wikidata Toolkit Command-line Tool</name>
+	<description>This module contains a command-line tool to process data dumps.</description>
+
+	<dependencies>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>wdtk-datamodel</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>wdtk-dumpfiles</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>wdtk-rdf</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>wdtk-util</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>wdtk-testing</artifactId>
+			<version>${project.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency> 
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-log4j12</artifactId>
+			<version>${slf4jVersion}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-compress</artifactId>
+			<version>${apacheCommonsCompressVersion}</version>
+		</dependency>
+		<dependency>
+			<groupId>commons-cli</groupId>
+			<artifactId>commons-cli</artifactId>
+			<version>${commonsCliVersion}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.ini4j</groupId>
+			<artifactId>ini4j</artifactId>
+			<version>${ini4JVersion}</version>
+		</dependency>
+	</dependencies>
+
+</project>
+
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/Client.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/Client.java
new file mode 100644
index 00000000..0f068cbd
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/Client.java
@@ -0,0 +1,297 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 - 2015 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import org.apache.commons.cli.ParseException;
+import org.apache.log4j.ConsoleAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.varia.LevelRangeFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wikidata.wdtk.datamodel.interfaces.Sites;
+import org.wikidata.wdtk.dumpfiles.DumpContentType;
+import org.wikidata.wdtk.dumpfiles.DumpProcessingController;
+import org.wikidata.wdtk.dumpfiles.EntityTimerProcessor;
+import org.wikidata.wdtk.dumpfiles.MwDumpFile;
+import org.wikidata.wdtk.util.DirectoryManager;
+import org.wikidata.wdtk.util.DirectoryManagerFactory;
+
+/**
+ * This class provides a Java command line client to process dump files.
+ *
+ * @author Michael Günther
+ * @author Markus Kroetzsch
+ */
+public class Client {
+
+	static final Logger logger = LoggerFactory.getLogger(Client.class);
+
+	/**
+	 * The pattern to use in log messages. One could insert the string
+	 * "%c{1}:%L" to also show class name and line.
+	 */
+	static final String LOG_PATTERN = "%d{yyyy-MM-dd HH:mm:ss} %-5p - %m%n";
+	static ConsoleAppender errorAppender = null;
+	static ConsoleAppender consoleAppender = null;
+
+	private Sites sites = null;
+
+	final DumpProcessingController dumpProcessingController;
+
+	protected final ClientConfiguration clientConfiguration;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param args
+	 *            command line arguments to configure the conversion
+	 */
+	public Client(DumpProcessingController dumpProcessingController,
+			String args[]) {
+		initializeLogging();
+		this.dumpProcessingController = dumpProcessingController;
+		this.clientConfiguration = new ClientConfiguration(args);
+
+		if (this.clientConfiguration.isQuiet()) {
+			consoleAppender.setThreshold(Level.OFF);
+		} else {
+			consoleAppender.setThreshold(Level.INFO);
+		}
+
+	}
+
+	/**
+	 * Performs all actions that have been configured.
+	 */
+	public void performActions() {
+		if (this.clientConfiguration.getActions().isEmpty()) {
+			this.clientConfiguration.printHelp();
+			return;
+		}
+
+		this.dumpProcessingController.setOfflineMode(this.clientConfiguration
+				.getOfflineMode());
+
+		if (this.clientConfiguration.getDumpDirectoryLocation() != null) {
+			try {
+				this.dumpProcessingController
+						.setDownloadDirectory(this.clientConfiguration
+								.getDumpDirectoryLocation());
+			} catch (IOException e) {
+				logger.error("Could not set download directory to "
+						+ this.clientConfiguration.getDumpDirectoryLocation()
+						+ ": " + e.getMessage());
+				logger.error("Aborting");
+				return;
+			}
+		}
+
+		dumpProcessingController.setLanguageFilter(this.clientConfiguration
+				.getFilterLanguages());
+		dumpProcessingController.setSiteLinkFilter(this.clientConfiguration
+				.getFilterSiteKeys());
+		dumpProcessingController.setPropertyFilter(this.clientConfiguration
+				.getFilterProperties());
+
+		MwDumpFile dumpFile = this.clientConfiguration.getLocalDumpFile();
+
+		if (dumpFile == null) {
+			dumpFile = dumpProcessingController
+					.getMostRecentDump(DumpContentType.JSON);
+		} else {
+			if (!dumpFile.isAvailable()) {
+				logger.error("Dump file not found or not readable: "
+						+ dumpFile.toString());
+				return;
+			}
+		}
+
+		this.clientConfiguration.setProjectName(dumpFile.getProjectName());
+		this.clientConfiguration.setDateStamp(dumpFile.getDateStamp());
+
+		boolean hasReadyProcessor = false;
+		for (DumpProcessingAction props : this.clientConfiguration.getActions()) {
+
+			if (!props.isReady()) {
+				continue;
+			}
+
+			if (props.needsSites()) {
+				prepareSites();
+				if (this.sites == null) { // sites unavailable
+					continue;
+				}
+				props.setSites(this.sites);
+			}
+			props.setDumpInformation(dumpFile.getProjectName(),
+					dumpFile.getDateStamp());
+			this.dumpProcessingController.registerEntityDocumentProcessor(
+					props, null, true);
+			hasReadyProcessor = true;
+		}
+
+		if (!hasReadyProcessor) {
+			return; // silent; non-ready action should report its problem
+					// directly
+		}
+
+		if (!this.clientConfiguration.isQuiet()) {
+			EntityTimerProcessor entityTimerProcessor = new EntityTimerProcessor(
+					0);
+			this.dumpProcessingController.registerEntityDocumentProcessor(
+					entityTimerProcessor, null, true);
+		}
+		openActions();
+		this.dumpProcessingController.processDump(dumpFile);
+		closeActions();
+
+		try {
+			writeReport();
+		} catch (IOException e) {
+			logger.error("Could not print report file: " + e.getMessage());
+		}
+
+	}
+
+	private void prepareSites() {
+		if (this.sites == null) {
+			try {
+				sites = this.dumpProcessingController.getSitesInformation();
+			} catch (IOException e) {
+				logger.error("Failed to get sites information: "
+						+ e.getMessage());
+			}
+		}
+	}
+
+	/**
+	 * Sets up Log4J to write log messages to the console. Low-priority messages
+	 * are logged to stdout while high-priority messages go to stderr.
+	 */
+	private void initializeLogging() {
+		// Since logging is static, make sure this is done only once even if
+		// multiple clients are created (e.g., during tests)
+		if (consoleAppender != null) {
+			return;
+		}
+
+		consoleAppender = new ConsoleAppender();
+		consoleAppender.setLayout(new PatternLayout(LOG_PATTERN));
+		consoleAppender.setThreshold(Level.INFO);
+		LevelRangeFilter filter = new LevelRangeFilter();
+		filter.setLevelMin(Level.TRACE);
+		filter.setLevelMax(Level.INFO);
+		consoleAppender.addFilter(filter);
+		consoleAppender.activateOptions();
+		org.apache.log4j.Logger.getRootLogger().addAppender(consoleAppender);
+
+		errorAppender = new ConsoleAppender();
+		errorAppender.setLayout(new PatternLayout(LOG_PATTERN));
+		errorAppender.setThreshold(Level.WARN);
+		errorAppender.setTarget(ConsoleAppender.SYSTEM_ERR);
+		errorAppender.activateOptions();
+		org.apache.log4j.Logger.getRootLogger().addAppender(errorAppender);
+	}
+
+	/**
+	 * Starts the work of each configured action by calling its
+	 * {@link DumpProcessingOutputAction#open()} method.
+	 */
+	private void openActions() {
+		for (DumpProcessingAction action : this.clientConfiguration
+				.getActions()) {
+			action.open();
+		}
+	}
+
+	/**
+	 * Writes a report file including the results of the
+	 * {@link DumpProcessingAction#getReport()} methods. If there is no report
+	 * filename specified the reports will be logged.
+	 *
+	 * @throws IOException
+	 */
+	void writeReport() throws IOException {
+		StringBuilder builder = new StringBuilder();
+		for (DumpProcessingAction action : this.clientConfiguration
+				.getActions()) {
+			if (this.clientConfiguration.getReportFileName() != null) {
+				builder.append(action.getActionName());
+				builder.append(": ");
+				if (action.isReady()) {
+					builder.append(action.getReport());
+				} else {
+					builder.append("Action was not executed.");
+				}
+				builder.append(System.getProperty("line.separator"));
+			} else {
+				logger.info(action.getActionName() + ": " + action.getReport());
+			}
+		}
+		if (this.clientConfiguration.getReportFileName() != null) {
+			Path outputDirectory = Paths.get(
+					this.clientConfiguration.getReportFileName()).getParent();
+			if (outputDirectory == null) {
+				outputDirectory = Paths.get(".");
+			}
+			DirectoryManager dm = DirectoryManagerFactory
+					.createDirectoryManager(outputDirectory, false);
+			OutputStream out = dm.getOutputStreamForFile(Paths
+					.get(this.clientConfiguration.getReportFileName())
+					.getFileName().toString());
+			out.write(builder.toString().getBytes(StandardCharsets.UTF_8));
+			out.close();
+		}
+	}
+
+	/**
+	 * Finishes the work of each configured action by calling its
+	 * {@link DumpProcessingOutputAction#close()} method.
+	 */
+	private void closeActions() {
+		for (DumpProcessingAction action : this.clientConfiguration
+				.getActions()) {
+			action.close();
+		}
+
+	}
+
+	/**
+	 * Launches the client with the specified parameters.
+	 *
+	 * @param args
+	 *            command line parameters
+	 * @throws ParseException
+	 * @throws IOException
+	 */
+	public static void main(String[] args) throws ParseException, IOException {
+		Client client = new Client(
+				new DumpProcessingController("wikidatawiki"), args);
+		client.performActions();
+	}
+}
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/ClientConfiguration.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/ClientConfiguration.java
new file mode 100644
index 00000000..8e1ce64f
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/ClientConfiguration.java
@@ -0,0 +1,887 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 - 2015 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionBuilder;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.ini4j.Ini;
+import org.ini4j.Profile.Section;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wikidata.wdtk.datamodel.helpers.Datamodel;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyIdValue;
+import org.wikidata.wdtk.dumpfiles.MwLocalDumpFile;
+
+/**
+ * This class handles the program arguments from the conversion command line
+ * tool.
+ *
+ * @author Michael Günther
+ * @author Markus Kroetzsch
+ */
+public class ClientConfiguration {
+
+	static final Logger logger = LoggerFactory
+			.getLogger(ClientConfiguration.class);
+
+	/**
+	 * Short command-line alternative to {@link #OPTION_HELP}.
+	 */
+	public static final String CMD_OPTION_HELP = "h";
+	/**
+	 * Short command-line alternative to {@link #OPTION_CONFIG_FILE}.
+	 */
+	public static final String CMD_OPTION_CONFIG_FILE = "c";
+	/**
+	 * Short command-line alternative to {@link #OPTION_DUMP_LOCATION}.
+	 */
+	public static final String CMD_OPTION_DUMP_LOCATION = "d";
+	/**
+	 * Short command-line alternative to {@link #OPTION_QUIET}.
+	 */
+	public static final String CMD_OPTION_QUIET = "q";
+	/**
+	 * Short command-line alternative to {@link #OPTION_OFFLINE_MODE}.
+	 */
+	public static final String CMD_OPTION_OFFLINE_MODE = "n";
+	/**
+	 * Short command-line alternative to {@link #OPTION_ACTION}.
+	 */
+	public static final String CMD_OPTION_ACTION = "a";
+	/**
+	 * Short command-line alternative to {@link #OPTION_OUTPUT_DESTINATION}.
+	 */
+	public static final String CMD_OPTION_OUTPUT_DESTINATION = "o";
+	/**
+	 * Short command-line alternative to {@link #OPTION_OUTPUT_COMPRESSION}.
+	 */
+	public static final String CMD_OPTION_OUTPUT_COMPRESSION = "z";
+	/**
+	 * Short command-line alternative to {@link #OPTION_OUTPUT_STDOUT}.
+	 */
+	public static final String CMD_OPTION_OUTPUT_STDOUT = "s";
+	/**
+	 * Short command-line alternative to {@link #OPTION_CREATE_REPORT}.
+	 */
+	public static final String CMD_OPTION_CREATE_REPORT = "r";
+	/**
+	 * Short command-line alternative to {@link #OPTION_LOCAL_DUMPFILE}.
+	 */
+	public static final String CMD_OPTION_LOCAL_DUMPFILE = "i";
+
+	/**
+	 * Name of the long command line option for printing the help text.
+	 */
+	public static final String OPTION_HELP = "help";
+	/**
+	 * Name of the long command line option for specifying the location of the
+	 * configuration file.
+	 */
+	public static final String OPTION_CONFIG_FILE = "config";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * specifying the base location of the dump files to use.
+	 */
+	public static final String OPTION_DUMP_LOCATION = "dumps";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * switching to offline mode.
+	 */
+	public static final String OPTION_OFFLINE_MODE = "offline";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * requesting that no messages are logged to stdout.
+	 */
+	public static final String OPTION_QUIET = "quiet";
+	/**
+	 * Name of the long command line option for selecting an action that should
+	 * be performed. Available actions are registered in the field
+	 * {@link #KNOWN_ACTIONS}.
+	 */
+	public static final String OPTION_ACTION = "action";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * specifying the language filters.
+	 */
+	public static final String OPTION_FILTER_LANGUAGES = "fLang";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * specifying the site link filters.
+	 */
+	public static final String OPTION_FILTER_SITES = "fSite";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * specifying the property filters.
+	 */
+	public static final String OPTION_FILTER_PROPERTIES = "fProp";
+	/**
+	 * Name of the long command line option to create a report file about the
+	 * files produced by DumpProcessingOutputActions.
+	 */
+	public static final String OPTION_CREATE_REPORT = "report";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * defining the destination (usually output file name) of actions that
+	 * produce output.
+	 */
+	public static final String OPTION_OUTPUT_DESTINATION = DumpProcessingOutputAction.OPTION_DESTINATION;
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * specifying the compression to use for the output.
+	 */
+	public static final String OPTION_OUTPUT_COMPRESSION = DumpProcessingOutputAction.OPTION_COMPRESSION;
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * directing output to stdout.
+	 */
+	public static final String OPTION_OUTPUT_STDOUT = DumpProcessingOutputAction.OPTION_USE_STDOUT;
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * specifying the tasks for RDF serialization.
+	 */
+	public static final String OPTION_OUTPUT_RDF_TYPE = "rdftasks";
+	/**
+	 * Name of the long command line option and configuration file field for
+	 * defining the path to a local dump file.
+	 */
+	public static final String OPTION_LOCAL_DUMPFILE = "input";
+
+	static final Map<String, Class<? extends DumpProcessingAction>> KNOWN_ACTIONS = new HashMap<>();
+	static {
+		KNOWN_ACTIONS.put("rdf", RdfSerializationAction.class);
+		KNOWN_ACTIONS.put("json", JsonSerializationAction.class);
+		KNOWN_ACTIONS.put("sqid", SchemaUsageAnalyzer.class);
+	}
+
+	/**
+	 * Available command-line options.
+	 */
+	static final Options options = new Options();
+	static {
+		initOptions();
+	}
+
+	/**
+	 * List of actions that were configured to be performed.
+	 */
+	final List<DumpProcessingAction> actions;
+
+	/**
+	 * Global configuration that defines if the operations should avoid all
+	 * internet access.
+	 */
+	boolean offlineMode = false;
+
+	/**
+	 * String representation of the directory where the dump files should be
+	 * sought.
+	 */
+	String dumpDirectoryLocation = null;
+
+	/**
+	 * Location of a dump file that should be processed.
+	 */
+	String inputDumpLocation = null;
+
+	/**
+	 * String representation of the path where the final report should be
+	 * stored.
+	 */
+	String reportFilename = null;
+
+	/**
+	 * True if no status messages should be written to stdout.
+	 */
+	boolean quiet = false;
+
+	/**
+	 * Set language codes to use as a filter, or null if no filter should be
+	 * used.
+	 */
+	Set<String> filterLanguages = null;
+
+	/**
+	 * Set site keys to use as a filter, or null if no filter should be used.
+	 */
+	Set<String> filterSites = null;
+
+	/**
+	 * Set property ids to use as a filter, or null if no filter should be used.
+	 */
+	Set<PropertyIdValue> filterProperties = null;
+
+	/**
+	 * Date stamp of the dump to be processed.
+	 */
+	String dateStamp = "UNKNOWN";
+	/**
+	 * String name of the site that the processed dump file comes from.
+	 */
+	String project = "UNKNOWN";
+
+	/**
+	 * Constructs a new object for the given arguments.
+	 *
+	 * @param args
+	 *            command-line arguments
+	 */
+	public ClientConfiguration(String[] args) {
+		this.actions = handleArguments(args);
+	}
+
+	/**
+	 * Inserts the information about the dateStamp of a dump and the project
+	 * name into a pattern.
+	 *
+	 * @param pattern
+	 *            String with wildcards
+	 * @param dateStamp
+	 * @param project
+	 * @return String with injected information.
+	 */
+	public static String insertDumpInformation(String pattern,
+			String dateStamp, String project) {
+		if (pattern == null) {
+			return null;
+		} else {
+			return pattern.replace("{DATE}", dateStamp).replace("{PROJECT}",
+					project);
+		}
+	}
+
+	/**
+	 * Returns the list of actions defined for this object.
+	 *
+	 * @return list of actions
+	 */
+	public List<DumpProcessingAction> getActions() {
+		return this.actions;
+	}
+
+	/**
+	 * Returns the location of the directory where downloaded dump files are
+	 * stored, or null if the default is to be used.
+	 *
+	 * @return string path to dump file directory
+	 */
+	public String getDumpDirectoryLocation() {
+		return this.dumpDirectoryLocation;
+	}
+
+	/**
+	 * Returns true if all operations should be performed in offline mode,
+	 * without accessing the Internet.
+	 *
+	 * @return true if in offline mode
+	 */
+	public boolean getOfflineMode() {
+		return this.offlineMode;
+	}
+
+	/**
+	 * Returns true if the application should not write anything to stdout. This
+	 * can be set explicitly or indirectly if one of the actions wants to write
+	 * to stdout.
+	 *
+	 * @return true if the application should not log messages to stdout
+	 */
+	public boolean isQuiet() {
+		return this.quiet;
+	}
+
+	/**
+	 * Returns the output destination where a report file should be created. If
+	 * the client should not create such a file the function will return null.
+	 *
+	 * @return report filename
+	 */
+	public String getReportFileName() {
+		return this.insertDumpInformation(this.reportFilename);
+	}
+
+	/**
+	 * Returns a set of language codes that should be used as a filter, or null
+	 * if no filter is set. An empty set means that all languages should be
+	 * filtered.
+	 *
+	 * @return language filter
+	 */
+	public Set<String> getFilterLanguages() {
+		return this.filterLanguages;
+	}
+
+	/**
+	 * Returns a set of site keys that should be used as a filter for site
+	 * links, or null if no filter is set. An empty set means that all site
+	 * links should be filtered.
+	 *
+	 * @return site key filter
+	 */
+	public Set<String> getFilterSiteKeys() {
+		return this.filterSites;
+	}
+
+	/**
+	 * Returns a set of property ids that should be used as a filter for
+	 * statements, or null if no filter is set. An empty set means that all
+	 * statements should be filtered.
+	 *
+	 * @return property filter
+	 */
+	public Set<PropertyIdValue> getFilterProperties() {
+		return this.filterProperties;
+	}
+
+	/**
+	 * Returns the string path to a dump file that should be processed.
+	 *
+	 * @return string path to file
+	 */
+	public String getInputDumpLocation() {
+		return this.inputDumpLocation;
+	}
+
+	/**
+	 * Returns a local dump file that was previously downloaded and should be
+	 * locally processed, or null if no local dump file is set.
+	 * <p>
+	 * The dump file object will be created based on the current configuration.
+	 *
+	 * @return dump file that should be locally processed
+	 */
+	public MwLocalDumpFile getLocalDumpFile() {
+		if (this.inputDumpLocation != null) {
+			return new MwLocalDumpFile(this.inputDumpLocation);
+		} else {
+			return null;
+		}
+	}
+
+	/**
+	 * Sets the project name of the dump file.
+	 *
+	 * @param project
+	 */
+	public void setProjectName(String project) {
+		this.project = project;
+	}
+
+	/**
+	 * Sets the date stamp of the dump file.
+	 *
+	 * @param dateStamp
+	 */
+	public void setDateStamp(String dateStamp) {
+		this.dateStamp = dateStamp;
+	}
+
+	/**
+	 * Returns the project name according to the dump file.
+	 *
+	 * @return project name
+	 */
+	public String getProjectName() {
+		return this.project;
+	}
+
+	/**
+	 * Returns the date stamp of the dump file.
+	 *
+	 * @return date stamp
+	 */
+	public String getDateStamp() {
+		return this.dateStamp;
+	}
+
+	/**
+	 * Prints a help text to the console.
+	 */
+	public void printHelp() {
+		HelpFormatter formatter = new HelpFormatter();
+		formatter.printHelp("wdtk-client", options);
+	}
+
+	public String insertDumpInformation(String pattern) {
+		return ClientConfiguration.insertDumpInformation(pattern,
+				this.dateStamp, this.project);
+	}
+
+	/**
+	 * This function interprets the arguments of the main function. By doing
+	 * this it will set flags for the dump generation. See in the help text for
+	 * more specific information about the options.
+	 *
+	 * @param args
+	 *            array of arguments from the main function.
+	 * @return list of {@link DumpProcessingOutputAction}
+	 */
+	private List<DumpProcessingAction> handleArguments(String[] args) {
+		CommandLine cmd;
+		CommandLineParser parser = new GnuParser();
+
+		try {
+			cmd = parser.parse(options, args);
+		} catch (ParseException e) {
+			logger.error("Failed to parse arguments: " + e.getMessage());
+			return Collections.emptyList();
+		}
+
+		// Stop processing if a help text is to be printed:
+		if ((cmd.hasOption(CMD_OPTION_HELP)) || (args.length == 0)) {
+			return Collections.emptyList();
+		}
+
+		List<DumpProcessingAction> configuration = new ArrayList<>();
+
+		handleGlobalArguments(cmd);
+
+		if (cmd.hasOption(CMD_OPTION_ACTION)) {
+			DumpProcessingAction action = handleActionArguments(cmd);
+			if (action != null) {
+				configuration.add(action);
+			}
+		}
+
+		if (cmd.hasOption(CMD_OPTION_CONFIG_FILE)) {
+			try {
+				List<DumpProcessingAction> configFile = readConfigFile(cmd
+						.getOptionValue(CMD_OPTION_CONFIG_FILE));
+				configuration.addAll(configFile);
+			} catch (IOException e) {
+				logger.error("Failed to read configuration file \""
+						+ cmd.getOptionValue(CMD_OPTION_CONFIG_FILE) + "\": "
+						+ e.toString());
+			}
+
+		}
+
+		return configuration;
+
+	}
+
+	/**
+	 *
+	 * Reads the properties defined in a configuration file. Returns a set of
+	 * configuration property blocks stored in {@link DumpProcessingAction}
+	 * objects and call
+	 * {@link ClientConfiguration#handleGlobalArguments(CommandLine)} to
+	 * interpreted the properties from the general section. The first element of
+	 * the list contains general information (about all dumps).
+	 *
+	 * @param path
+	 *            filename and path of the configuration file
+	 * @return the list with configurations for all output dumps
+	 * @throws IOException
+	 */
+	private List<DumpProcessingAction> readConfigFile(String path)
+			throws IOException {
+		List<DumpProcessingAction> result = new ArrayList<>();
+
+		try(FileReader reader = new FileReader(path)) {
+			Ini ini = new Ini(reader);
+
+			for (Section section : ini.values()) {
+				if (section.getName().toLowerCase().equals("general")) {
+					handleGlobalArguments(section);
+				} else {
+					DumpProcessingAction action = handleActionArguments(section);
+					if (action != null) {
+						action.setActionName(section.getName());
+						result.add(action);
+					}
+				}
+			}
+			return result;
+		}
+	}
+
+	/**
+	 * Analyses the command-line arguments which are relevant for the
+	 * serialization process in general. It fills out the class arguments with
+	 * this data.
+	 *
+	 * @param cmd
+	 *            {@link CommandLine} objects; contains the command line
+	 *            arguments parsed by a {@link CommandLineParser}
+	 */
+	private void handleGlobalArguments(CommandLine cmd) {
+		if (cmd.hasOption(CMD_OPTION_DUMP_LOCATION)) {
+			this.dumpDirectoryLocation = cmd
+					.getOptionValue(CMD_OPTION_DUMP_LOCATION);
+		}
+
+		if (cmd.hasOption(CMD_OPTION_OFFLINE_MODE)) {
+			this.offlineMode = true;
+		}
+
+		if (cmd.hasOption(CMD_OPTION_QUIET)) {
+			this.quiet = true;
+		}
+
+		if (cmd.hasOption(CMD_OPTION_CREATE_REPORT)) {
+			this.reportFilename = cmd.getOptionValue(CMD_OPTION_CREATE_REPORT);
+		}
+
+		if (cmd.hasOption(OPTION_FILTER_LANGUAGES)) {
+			setLanguageFilters(cmd.getOptionValue(OPTION_FILTER_LANGUAGES));
+		}
+
+		if (cmd.hasOption(OPTION_FILTER_SITES)) {
+			setSiteFilters(cmd.getOptionValue(OPTION_FILTER_SITES));
+		}
+
+		if (cmd.hasOption(OPTION_FILTER_PROPERTIES)) {
+			setPropertyFilters(cmd.getOptionValue(OPTION_FILTER_PROPERTIES));
+		}
+
+		if (cmd.hasOption(CMD_OPTION_LOCAL_DUMPFILE)) {
+			this.inputDumpLocation = cmd.getOptionValue(OPTION_LOCAL_DUMPFILE);
+		}
+	}
+
+	/**
+	 * Analyses the content of the general section of an ini configuration file
+	 * and fills out the class arguments with this data.
+	 *
+	 * @param section
+	 *            {@link Section} with name "general"
+	 */
+	private void handleGlobalArguments(Section section) {
+		for (String key : section.keySet()) {
+			switch (key) {
+			case OPTION_OFFLINE_MODE:
+				if (section.get(key).toLowerCase().equals("true")) {
+					this.offlineMode = true;
+				}
+				break;
+			case OPTION_QUIET:
+				if (section.get(key).toLowerCase().equals("true")) {
+					this.quiet = true;
+				}
+				break;
+			case OPTION_CREATE_REPORT:
+				this.reportFilename = section.get(key);
+				break;
+			case OPTION_DUMP_LOCATION:
+				this.dumpDirectoryLocation = section.get(key);
+				break;
+			case OPTION_FILTER_LANGUAGES:
+				setLanguageFilters(section.get(key));
+				break;
+			case OPTION_FILTER_SITES:
+				setSiteFilters(section.get(key));
+				break;
+			case OPTION_FILTER_PROPERTIES:
+				setPropertyFilters(section.get(key));
+				break;
+			case OPTION_LOCAL_DUMPFILE:
+				this.inputDumpLocation = section.get(key);
+				break;
+			default:
+				logger.warn("Unrecognized option: " + key);
+			}
+		}
+	}
+
+	/**
+	 * Analyses the command-line arguments which are relevant for the specific
+	 * action that is to be executed, and returns a corresponding
+	 * {@link DumpProcessingAction} object.
+	 *
+	 * @param cmd
+	 *            {@link CommandLine} objects; contains the command line
+	 *            arguments parsed by a {@link CommandLineParser}
+	 * @return {@link DumpProcessingAction} for the given arguments
+	 */
+	private DumpProcessingAction handleActionArguments(CommandLine cmd) {
+
+		DumpProcessingAction result = makeDumpProcessingAction(cmd
+				.getOptionValue(CMD_OPTION_ACTION).toLowerCase());
+		if (result == null) {
+			return null;
+		}
+
+		for (Option option : cmd.getOptions()) {
+			result.setOption(option.getLongOpt(), option.getValue());
+		}
+
+		checkDuplicateStdOutOutput(result);
+
+		return result;
+	}
+
+	/**
+	 * Analyses the content of a section of an ini configuration file (not the
+	 * "general" section) and fills out the class arguments of an new
+	 * {@link DumpProcessingAction} with this data.
+	 *
+	 * @param section
+	 *            {@link Section} with name "general"
+	 * @return {@link DumpProcessingAction} containing the output parameters
+	 */
+	private DumpProcessingAction handleActionArguments(Section section) {
+		DumpProcessingAction result = makeDumpProcessingAction(section.get(
+				OPTION_ACTION).toLowerCase());
+		if (result == null) {
+			return null;
+		}
+
+		for (String key : section.keySet()) {
+			if (!result.setOption(key.toLowerCase(), section.get(key))
+					&& !OPTION_ACTION.equals(key.toLowerCase())) {
+				logger.warn("Unrecognized option: " + key);
+			}
+		}
+
+		checkDuplicateStdOutOutput(result);
+
+		return result;
+	}
+
+	/**
+	 * Checks if a newly created action wants to write output to stdout, and
+	 * logs a warning if other actions are doing the same.
+	 *
+	 * @param newAction
+	 *            the new action to be checked
+	 */
+	private void checkDuplicateStdOutOutput(DumpProcessingAction newAction) {
+		if (newAction.useStdOut()) {
+			if (this.quiet) {
+				logger.warn("Multiple actions are using stdout as output destination.");
+			}
+			this.quiet = true;
+		}
+	}
+
+	/**
+	 * Creates a {@link DumpProcessingAction} object for the action of the given
+	 * name. The operation may fail if the name is not associated with any
+	 * action, or if the associated action class cannot be instantiated.
+	 *
+	 * @param name
+	 *            of the action
+	 * @return the {@link DumpProcessingAction} or null if creation failed
+	 */
+	private DumpProcessingAction makeDumpProcessingAction(String name) {
+		if (!KNOWN_ACTIONS.containsKey(name)) {
+			logger.error("Unknown action \"" + name + "\".");
+			return null;
+		}
+
+		try {
+			Constructor<? extends DumpProcessingAction> constructor = KNOWN_ACTIONS
+					.get(name).getConstructor();
+			return constructor.newInstance();
+		} catch (NoSuchMethodException | SecurityException
+				| IllegalArgumentException | IllegalAccessException e) {
+			logger.error("Class \"" + KNOWN_ACTIONS.get(name)
+					+ "\" that was registered to handle action \"" + name
+					+ "\" does not have an appropriate constructor.");
+			return null;
+		} catch (InstantiationException | InvocationTargetException e) {
+			logger.error("Error when trying to instantiate handler for action \""
+					+ name + "\":" + e.getMessage());
+			return null;
+		}
+
+	}
+
+	/**
+	 * Sets the set of language filters based on the given string.
+	 *
+	 * @param filters
+	 *            comma-separates list of language codes, or "-" to filter all
+	 *            languages
+	 */
+	private void setLanguageFilters(String filters) {
+		this.filterLanguages = new HashSet<>();
+		if (!"-".equals(filters)) {
+			Collections.addAll(this.filterLanguages, filters.split(","));
+		}
+	}
+
+	/**
+	 * Sets the set of site filters based on the given string.
+	 *
+	 * @param filters
+	 *            comma-separates list of site keys, or "-" to filter all site
+	 *            links
+	 */
+	private void setSiteFilters(String filters) {
+		this.filterSites = new HashSet<>();
+		if (!"-".equals(filters)) {
+			Collections.addAll(this.filterSites, filters.split(","));
+		}
+	}
+
+	/**
+	 * Sets the set of property filters based on the given string.
+	 *
+	 * @param filters
+	 *            comma-separates list of property ids, or "-" to filter all
+	 *            statements
+	 */
+	private void setPropertyFilters(String filters) {
+		this.filterProperties = new HashSet<>();
+		if (!"-".equals(filters)) {
+			for (String pid : filters.split(",")) {
+				this.filterProperties.add(Datamodel
+						.makeWikidataPropertyIdValue(pid));
+			}
+		}
+	}
+
+	/**
+	 * Builds a list of legal options and store them into the options objects.
+	 */
+	@SuppressWarnings("static-access")
+	private static void initOptions() {
+
+		List<String> actions = new ArrayList<>(KNOWN_ACTIONS.keySet());
+		Collections.sort(actions);
+		Option action = OptionBuilder
+				.hasArg()
+				.withArgName("action")
+				.withDescription(
+						"define the action that should be performed; avaible actions: "
+								+ actions).withLongOpt(OPTION_ACTION)
+				.create(CMD_OPTION_ACTION);
+
+		Option destination = OptionBuilder.withArgName("path").hasArg()
+				.withDescription("place the output into the file at <path>")
+				.withLongOpt(OPTION_OUTPUT_DESTINATION)
+				.create(CMD_OPTION_OUTPUT_DESTINATION);
+
+		Option dumplocation = OptionBuilder
+				.hasArg()
+				.withArgName("path")
+				.withDescription(
+						"set the directory where downloaded dump files are stored")
+				.withLongOpt(OPTION_DUMP_LOCATION)
+				.create(CMD_OPTION_DUMP_LOCATION);
+
+		Option config = OptionBuilder
+				.hasArg()
+				.withArgName("file")
+				.withDescription(
+						"set a config file; use this to define multiple actions for a single run")
+				.withLongOpt(OPTION_CONFIG_FILE).create(CMD_OPTION_CONFIG_FILE);
+
+		Option rdfdump = OptionBuilder
+				.hasArgs()
+				.withArgName("task")
+				.withDescription(
+						"specify which data to include in RDF dump (use with action \"rdf\"); run with options \"-a rdf -n\" for help")
+				.withLongOpt(OPTION_OUTPUT_RDF_TYPE).create();
+
+		Option filterLanguages = OptionBuilder
+				.hasArgs()
+				.withArgName("languages")
+				.withDescription(
+						"specifies a list of language codes; if given, only terms in languages in this list will be processed; the value \"-\" denotes the empty list (no terms are processed)")
+				.withLongOpt(OPTION_FILTER_LANGUAGES).create();
+
+		Option filterSites = OptionBuilder
+				.hasArgs()
+				.withArgName("sites")
+				.withDescription(
+						"specifies a list of site keys; if given, only site links to sites in this list will be processed; the value \"-\" denotes the empty list (no site links are processed)")
+				.withLongOpt(OPTION_FILTER_SITES).create();
+
+		Option filterProperties = OptionBuilder
+				.hasArgs()
+				.withArgName("ids")
+				.withDescription(
+						"specifies a list of property ids; if given, only statements for properties in this list will be processed; the value \"-\" denotes the empty list (no statements are processed)")
+				.withLongOpt(OPTION_FILTER_PROPERTIES).create();
+
+		Option compressionExtention = OptionBuilder
+				.hasArg()
+				.withArgName("type")
+				.withDescription(
+						"define a compression format to be used for the output; possible values: "
+								+ DumpProcessingOutputAction.COMPRESS_GZIP
+								+ ", "
+								+ DumpProcessingOutputAction.COMPRESS_BZ2)
+				.withLongOpt(OPTION_OUTPUT_COMPRESSION)
+				.create(CMD_OPTION_OUTPUT_COMPRESSION);
+
+		Option report = OptionBuilder
+				.hasArg()
+				.withArgName("path")
+				.withDescription(
+						"specifies a path to print a final report after dump generations")
+				.withLongOpt(OPTION_CREATE_REPORT)
+				.create(CMD_OPTION_CREATE_REPORT);
+
+		Option localDump = OptionBuilder
+				.hasArg()
+				.withArgName("path")
+				.withDescription(
+						"select a dump file for processing; if omitted, then the latest dump from Wikidata will be used (and possibly downloaded)")
+				.withLongOpt(OPTION_LOCAL_DUMPFILE)
+				.create(CMD_OPTION_LOCAL_DUMPFILE);
+
+		options.addOption(config);
+		options.addOption(action);
+		options.addOption(
+				CMD_OPTION_QUIET,
+				OPTION_QUIET,
+				false,
+				"perform all actions quietly, without printing status messages to the console; errors/warnings are still printed to stderr");
+		options.addOption(destination);
+		options.addOption(dumplocation);
+		options.addOption(filterLanguages);
+		options.addOption(filterSites);
+		options.addOption(filterProperties);
+		options.addOption(compressionExtention);
+		options.addOption(report);
+		options.addOption(localDump);
+		options.addOption(rdfdump);
+		options.addOption(CMD_OPTION_OFFLINE_MODE, OPTION_OFFLINE_MODE, false,
+				"execute all operations in offline mode, especially do not download new dumps");
+		options.addOption(CMD_OPTION_HELP, OPTION_HELP, false,
+				"print this message");
+
+		options.addOption(CMD_OPTION_OUTPUT_STDOUT, OPTION_OUTPUT_STDOUT,
+				false, "write output to stdout");
+	}
+
+}
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/DumpProcessingAction.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/DumpProcessingAction.java
new file mode 100644
index 00000000..df7c9165
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/DumpProcessingAction.java
@@ -0,0 +1,134 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 - 2015 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.wikidata.wdtk.datamodel.interfaces.EntityDocumentDumpProcessor;
+import org.wikidata.wdtk.datamodel.interfaces.Sites;
+
+/**
+ * This interface represents an action that may be performed with Wikibase
+ * dumps. The processing as such is achieved by implementing
+ * {@link EntityDocumentDumpProcessor}. Additional methods provide a generic
+ * interface for setting options and other auxiliary information that the action
+ * might require.
+ * 
+ * @author Markus Kroetzsch
+ * 
+ */
+public interface DumpProcessingAction extends EntityDocumentDumpProcessor {
+
+	/**
+	 * Returns true if processing requires a {@link Sites} object to be set.
+	 * This is the case for some operations that process site links.
+	 * 
+	 * @return true if sites information is needed.
+	 */
+	boolean needsSites();
+
+	/**
+	 * Returns true if the action is ready to process a dump. An action that is
+	 * insufficiently or wrongly configured can return false here to avoid being
+	 * run.
+	 * <p>
+	 * If this method is called on an action that is not ready, the action
+	 * should print helpful information on the missing configuration to stdout
+	 * as a side effect.
+	 * 
+	 * @return true if ready to run
+	 */
+	boolean isReady();
+
+	/**
+	 * Sets the sites information to the given value. The method
+	 * {@link #needsSites()} is used to find out if this is actually needed.
+	 * 
+	 * @param sites
+	 *            the sites information for the data that will be processed
+	 */
+	void setSites(Sites sites);
+
+	/**
+	 * Sets the options of the specified name to the given value. Returns true
+	 * if the option was known, and false otherwise. Implementation should
+	 * overwrite this function to support additional options.
+	 * 
+	 * @param option
+	 *            name of the option to be set
+	 * @param value
+	 *            the new value of the option
+	 * @return true if the option was known (no matter if the given value could
+	 *         actually be used or not)
+	 */
+	boolean setOption(String option, String value);
+
+	/**
+	 * Returns true if this action will write results (not log messages) to
+	 * stdout. The default implementation returns true if no other output
+	 * destination has been specified. Subclasses that do not write results to
+	 * stdout in this case should overwrite this method.
+	 * 
+	 * @return true if the action is configured to write results to stdout
+	 */
+	boolean useStdOut();
+
+	/**
+	 * Provides the action with general information about the dump that is to be
+	 * processed. This may be used, e.g., to define file names to use for the
+	 * output.
+	 * 
+	 * @param project
+	 *            the name of the project that the dump is from
+	 * @param dateStamp
+	 *            the datestamp (YYYYMMDD) for the dump
+	 */
+	void setDumpInformation(String project, String dateStamp);
+
+	/**
+	 * Returns a report message containing information about the files which
+	 * were generated.
+	 * 
+	 * @return report message
+	 */
+	String getReport();
+
+	/**
+	 * Sets the name of the action. If this is not set, a default name will be
+	 * used.
+	 * 
+	 * @param name
+	 */
+	void setActionName(String name);
+
+	/**
+	 * Returns the name of the action.
+	 * 
+	 * @return name
+	 */
+	String getActionName();
+
+	/**
+	 * Returns the default name for an action.
+	 * 
+	 * @return default name
+	 */
+	String getDefaultActionName();
+
+}
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/DumpProcessingOutputAction.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/DumpProcessingOutputAction.java
new file mode 100644
index 00000000..27ae7b03
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/DumpProcessingOutputAction.java
@@ -0,0 +1,358 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 - 2015 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipParameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wikidata.wdtk.datamodel.interfaces.Sites;
+import org.wikidata.wdtk.util.DirectoryManager;
+import org.wikidata.wdtk.util.DirectoryManagerFactory;
+
+/**
+ * Abstract implementation of {@link DumpProcessingAction} that represents
+ * actions which generate outputs (in the form of files). It supports options
+ * {@link #OPTION_DESTINATION}, {@link #OPTION_COMPRESSION}, and
+ * {@link #OPTION_USE_STDOUT}. Moreover, it provides some static helper
+ * functions for opening files for writing.
+ *
+ * @author Michael Günther
+ * @author Markus Kroetzsch
+ */
+public abstract class DumpProcessingOutputAction implements
+		DumpProcessingAction {
+
+	static final Logger logger = LoggerFactory
+			.getLogger(DumpProcessingAction.class);
+
+	/**
+	 * Name of the option used to define that output be compressed in a
+	 * particular way. By default, output is not compressed. When using
+	 * compression, the output file will be extended with a suitable file
+	 * extension.
+	 */
+	public static final String OPTION_COMPRESSION = "compression";
+	/**
+	 * Name of the option to set the destination file name (may include path)
+	 * for outputs generated by this action. If not given, actions should pick a
+	 * default output file.
+	 */
+	public static final String OPTION_DESTINATION = "output";
+	/**
+	 * Name of the option to redirect output to stdout. This is achieved by
+	 * setting the value to the string "true". Other values are ignored.
+	 */
+	public static final String OPTION_USE_STDOUT = "stdout";
+
+	public static final String COMPRESS_BZ2 = "bz2";
+	public static final String COMPRESS_GZIP = "gz";
+	public static final String COMPRESS_NONE = "";
+
+	/**
+	 * Output streams that were created by this class. If close is called, it
+	 * will close all of them properly.
+	 *
+	 */
+	protected Set<Closeable> outputStreams = new HashSet<>();
+
+	/**
+	 * The name of the action.
+	 */
+	protected String name;
+
+	/**
+	 * The {@link Sites} object if provided.
+	 *
+	 * @see #needsSites()
+	 */
+	protected Sites sites;
+
+	/**
+	 * For operations that generate output, this is the name of the output file,
+	 * or null to use the default file name for the given options.
+	 */
+	protected String outputDestination = null;
+
+	/**
+	 * Specifies if output should be redirected to stdout rather than being
+	 * written to a file.
+	 */
+	protected boolean useStdOut = false;
+
+	/**
+	 * String to indicate the output compression to be used, if any.
+	 */
+	protected String compressionType = COMPRESS_NONE;
+
+	/**
+	 * Date stamp of the dump to be processed.
+	 */
+	protected String dateStamp = "UNKNOWN";
+	/**
+	 * String name of the site that the processed dump file comes from.
+	 */
+	protected String project = "UNKNOWN";
+
+	@Override
+	public void setSites(Sites sites) {
+		this.sites = sites;
+	}
+
+	@Override
+	public boolean setOption(String option, String value) {
+		switch (option) {
+		case OPTION_DESTINATION:
+			this.outputDestination = value;
+			return true;
+		case OPTION_COMPRESSION:
+			this.compressionType = (value == null) ? COMPRESS_NONE : value.toLowerCase();
+			return true;
+		case OPTION_USE_STDOUT:
+			this.useStdOut = (value == null) || "true".equals(value);
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	@Override
+	public boolean useStdOut() {
+		return this.useStdOut;
+	}
+
+	@Override
+	public void setDumpInformation(String project, String dateStamp) {
+		this.project = project;
+		this.dateStamp = dateStamp;
+	}
+
+	@Override
+	public void setActionName(String name) {
+		this.name = name;
+	}
+
+	@Override
+	public String getActionName() {
+		if (this.name != null) {
+			return this.name;
+		} else {
+			return getDefaultActionName();
+		}
+	}
+
+	public String getOutputFilename() {
+		if (this.outputDestination == null) {
+			return "unnamed.out";
+		} else {
+			return insertDumpInformation(this.outputDestination);
+		}
+	}
+
+	public String insertDumpInformation(String pattern) {
+		return ClientConfiguration.insertDumpInformation(pattern,
+				this.dateStamp, this.project);
+	}
+
+	@Override
+	public void close() {
+		for (Closeable closeable : this.outputStreams) {
+			DumpProcessingOutputAction.close(closeable);
+		}
+	}
+
+	/**
+	 * Creates an compressing {@link OutputStream}. The result is owned by the
+	 * caller and should be closed later. Neverhteless, the {@link #close()}
+	 * method of this class must also be called, since it may free additional
+	 * resources created.
+	 *
+	 * @param useStdOut
+	 *            if true, {@link System#out} is returned and the other
+	 *            parameters are ignored
+	 * @param filePath
+	 *            the string name of the output file, possibly including path
+	 *            information
+	 * @param compressionType
+	 *            a string that refers to a type of output compression or the
+	 *            empty string (no compression); a suitable file extension will
+	 *            be added to the output file
+	 *
+	 * @return compressing {@link OutputStream}
+	 * @throws IOException
+	 *             if there were problems opening the required streams
+	 */
+	protected OutputStream getOutputStream(boolean useStdOut, String filePath,
+			String compressionType) throws IOException {
+		if (useStdOut) {
+			return System.out;
+		}
+
+		if (!compressionType.isEmpty()) {
+			filePath += "." + compressionType;
+		}
+
+		Path outputDirectory = Paths.get(filePath).getParent();
+		if (outputDirectory == null) {
+			outputDirectory = Paths.get(".");
+		}
+
+		DirectoryManager dm = DirectoryManagerFactory.createDirectoryManager(
+				outputDirectory, false);
+		OutputStream out = dm.getOutputStreamForFile(Paths.get(filePath)
+				.getFileName().toString());
+
+		OutputStream bufferedFileOutputStream = new BufferedOutputStream(out,
+				1024 * 1024 * 5);
+
+		switch (compressionType) {
+		case COMPRESS_BZ2:
+			return getAsynchronousOutputStream(new BZip2CompressorOutputStream(
+					bufferedFileOutputStream));
+		case COMPRESS_GZIP:
+			GzipParameters gzipParameters = new GzipParameters();
+			gzipParameters.setCompressionLevel(7);
+			return getAsynchronousOutputStream(new GzipCompressorOutputStream(
+					bufferedFileOutputStream, gzipParameters));
+		case COMPRESS_NONE:
+			return bufferedFileOutputStream;
+		default:
+			bufferedFileOutputStream.close();
+			throw new IllegalArgumentException(
+					"Unsupported compression format: " + compressionType);
+		}
+	}
+
+	/**
+	 * Simple interface for a Runnable that can be stopped gracefully by calling
+	 * a method {@link FinishableRunnable#finish()}.
+	 *
+	 * @author Markus Kroetzsch
+	 *
+	 */
+	protected interface FinishableRunnable extends Runnable {
+
+		/**
+		 * Finishes the current operation gracefully. The method will wait until
+		 * the thread has really finished.
+		 */
+		void finish();
+	}
+
+	/**
+	 * Creates a separate thread for writing into the given output stream and
+	 * returns a pipe output stream that can be used to pass data to this
+	 * thread.
+	 * <p>
+	 * This code is inspired by
+	 * http://stackoverflow.com/questions/12532073/gzipoutputstream
+	 * -that-does-its-compression-in-a-separate-thread
+	 *
+	 * @param outputStream
+	 *            the stream to write to in the thread
+	 * @return a new stream that data should be written to
+	 * @throws IOException
+	 *             if the pipes could not be created for some reason
+	 */
+	protected OutputStream getAsynchronousOutputStream(
+			final OutputStream outputStream) throws IOException {
+		final int SIZE = 1024 * 1024 * 10;
+		final PipedOutputStream pos = new PipedOutputStream();
+		final PipedInputStream pis = new PipedInputStream(pos, SIZE);
+
+		final FinishableRunnable run = new FinishableRunnable() {
+
+			volatile boolean finish = false;
+			volatile boolean hasFinished = false;
+
+			@Override
+			public void finish() {
+				this.finish = true;
+				while (!this.hasFinished) {
+					// loop until thread is really finished
+				}
+			}
+
+			@Override
+			public void run() {
+				try {
+					byte[] bytes = new byte[SIZE];
+					// Note that we finish really gently here, writing all data
+					// that is still in the input first (in theory, new data
+					// could arrive asynchronously, so that the thread never
+					// finishes, but this is not the intended mode of
+					// operation).
+					for (int len; (!this.finish || pis.available() > 0)
+							&& (len = pis.read(bytes)) > 0;) {
+						outputStream.write(bytes, 0, len);
+					}
+				} catch (IOException e) {
+					e.printStackTrace();
+				} finally {
+					close(pis);
+					close(outputStream);
+					this.hasFinished = true;
+				}
+			}
+		};
+
+		new Thread(run, "async-output-stream").start();
+
+		this.outputStreams.add(new Closeable() {
+			@Override
+			public void close() throws IOException {
+				run.finish();
+			}
+		});
+
+		return pos;
+	}
+
+	/**
+	 * Closes a Closeable and swallows any exceptions that might occur in the
+	 * process.
+	 *
+	 * @param closeable
+	 */
+	private static void close(Closeable closeable) {
+		if (closeable != null) {
+			try {
+				closeable.close();
+			} catch (IOException ignored) {
+				logger.error("Failed to close output stream: "
+						+ ignored.getMessage());
+			}
+		}
+	}
+
+}
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/JsonSerializationAction.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/JsonSerializationAction.java
new file mode 100644
index 00000000..ddcd38da
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/JsonSerializationAction.java
@@ -0,0 +1,131 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.wikidata.wdtk.datamodel.helpers.JsonSerializer;
+import org.wikidata.wdtk.datamodel.interfaces.ItemDocument;
+import org.wikidata.wdtk.datamodel.interfaces.LexemeDocument;
+import org.wikidata.wdtk.datamodel.interfaces.MediaInfoDocument;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyDocument;
+
+/**
+ * This class represents an action of generating a JSON dump from data. It has
+ * no specific options.
+ *
+ * @author Markus Kroetzsch
+ *
+ */
+public class JsonSerializationAction extends DumpProcessingOutputAction {
+
+	/**
+	 * The base file name that will be used by default. File endings for
+	 * indicating compression will be appended where required.
+	 */
+	public final static String DEFAULT_FILE_NAME = "{PROJECT}-{DATE}.json";
+
+	/**
+	 * default action name is used to separate different
+	 * DumpProcessingOutputActions from each other.
+	 */
+	public final static String DEFAULT_ACTION_NAME = "JsonSerializationAction";
+
+	/**
+	 * The actual serializer used internally.
+	 */
+	JsonSerializer serializer;
+
+	/**
+	 * Constructor. See {@link DumpProcessingOutputAction} for more details.
+	 */
+	public JsonSerializationAction() {
+		this.outputDestination = DEFAULT_FILE_NAME;
+	}
+
+	@Override
+	public boolean needsSites() {
+		return false;
+	}
+
+	@Override
+	public boolean isReady() {
+		return true; // can always run
+	}
+
+	@Override
+	public void open() {
+		OutputStream outputStream;
+		try {
+			outputStream = getOutputStream(this.useStdOut, getOutputFilename(),
+					this.compressionType);
+			this.serializer = new JsonSerializer(outputStream);
+			this.serializer.open();
+		} catch (IOException e) {
+			// TODO rather print error and set a nonce processor here
+			e.printStackTrace();
+		}
+	}
+
+	@Override
+	public void processItemDocument(ItemDocument itemDocument) {
+		this.serializer.processItemDocument(itemDocument);
+	}
+
+	@Override
+	public void processPropertyDocument(PropertyDocument propertyDocument) {
+		this.serializer.processPropertyDocument(propertyDocument);
+	}
+
+	@Override
+	public void processLexemeDocument(LexemeDocument lexemeDocument) {
+		this.serializer.processLexemeDocument(lexemeDocument);
+	}
+
+	@Override
+	public void processMediaInfoDocument(MediaInfoDocument mediaInfoDocument) {
+		this.serializer.processMediaInfoDocument(mediaInfoDocument);
+	}
+
+	@Override
+	public void close() {
+		this.serializer.close();
+		super.close();
+	}
+
+	@Override
+	public String getReport() {
+		String message = "Finished serialization of "
+				+ this.serializer.getEntityDocumentCount()
+				+ " EntityDocuments in file " + getOutputFilename();
+		if (!this.compressionType.equals(COMPRESS_NONE)) {
+			message += "." + this.compressionType;
+		}
+		return message;
+	}
+
+	@Override
+	public String getDefaultActionName() {
+		return DEFAULT_ACTION_NAME;
+	}
+
+}
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/RdfSerializationAction.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/RdfSerializationAction.java
new file mode 100644
index 00000000..c54b46ed
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/RdfSerializationAction.java
@@ -0,0 +1,299 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.text.WordUtils;
+import org.eclipse.rdf4j.rio.RDFFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wikidata.wdtk.datamodel.interfaces.ItemDocument;
+import org.wikidata.wdtk.datamodel.interfaces.LexemeDocument;
+import org.wikidata.wdtk.datamodel.interfaces.MediaInfoDocument;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyDocument;
+import org.wikidata.wdtk.rdf.PropertyRegister;
+import org.wikidata.wdtk.rdf.RdfSerializer;
+
+/**
+ * This class represents an action of generating an RDF dump from data. It
+ * provides the additional option
+ * {@link RdfSerializationAction#OPTION_RDF_TASKS}, which is required for
+ * generating any output.
+ *
+ * @author Markus Kroetzsch
+ *
+ */
+public class RdfSerializationAction extends DumpProcessingOutputAction {
+
+	static final Logger logger = LoggerFactory
+			.getLogger(DumpProcessingOutputAction.class);
+
+	/**
+	 * Name of the option that defines the tasks of the RDF serializer. The
+	 * value of this option should be a space-free, comma-separated list of task
+	 * names. Tasks define the data that should be processed ("items",
+	 * "properties", "entities") and what information should be written for each
+	 * ("statements", "sitelinks", "datatypes", "labels", "desciptions",
+	 * "aliases", "terms" meaning "labels,descriptions,aliases", "alldata"
+	 * meaning "statements,terms,sitelinks,datatypes","simplestatements").
+	 */
+	public static final String OPTION_RDF_TASKS = "rdftasks";
+
+	public static final Map<String, Integer> KNOWN_TASKS = new HashMap<>();
+	static {
+		KNOWN_TASKS.put("items", RdfSerializer.TASK_ITEMS);
+		KNOWN_TASKS.put("properties", RdfSerializer.TASK_PROPERTIES);
+		KNOWN_TASKS.put("entities", RdfSerializer.TASK_ALL_ENTITIES);
+		KNOWN_TASKS.put("alldata", RdfSerializer.TASK_ALL_EXACT_DATA);
+		KNOWN_TASKS.put("statements", RdfSerializer.TASK_STATEMENTS);
+		KNOWN_TASKS.put("sitelinks", RdfSerializer.TASK_SITELINKS);
+		KNOWN_TASKS.put("datatypes", RdfSerializer.TASK_DATATYPES);
+		KNOWN_TASKS.put("labels", RdfSerializer.TASK_LABELS);
+		KNOWN_TASKS.put("descriptions", RdfSerializer.TASK_DESCRIPTIONS);
+		KNOWN_TASKS.put("aliases", RdfSerializer.TASK_ALIASES);
+		KNOWN_TASKS.put("terms", RdfSerializer.TASK_TERMS);
+		KNOWN_TASKS
+				.put("interpropertylinks", RdfSerializer.TASK_PROPERTY_LINKS);
+		KNOWN_TASKS.put("simplestatements",
+				RdfSerializer.TASK_SIMPLE_STATEMENTS);
+	}
+
+	public static final Map<String, String> TASK_HELP = new HashMap<>();
+	static {
+		TASK_HELP.put("items", "consider items when exporting data");
+		TASK_HELP.put("properties", "consider properties when exporting data");
+		TASK_HELP.put("entities", "consider all entities when exporting data");
+		TASK_HELP.put("alldata",
+				"export all data available for the considered entities");
+		TASK_HELP.put("statements",
+				"export statements for the considered entities");
+		TASK_HELP.put("sitelinks",
+				"export site links for the considered entities");
+		TASK_HELP.put("datatypes",
+				"export datatypes for the considered entities");
+		TASK_HELP.put("labels", "export labels for the considered entities");
+		TASK_HELP.put("descriptions",
+				"export descriptions for the considered entities");
+		TASK_HELP.put("aliases", "export labels for the considered entities");
+		TASK_HELP.put("terms", "shortcut for labels,descriptions,aliases");
+		TASK_HELP
+				.put("simplestatements",
+						"export unqualified statements without references as single triples");
+		TASK_HELP
+				.put("interpropertylinks",
+						"export triples which connect wikidata properties and there respective rdf properties in different contexts");
+	}
+
+	/**
+	 * The base file name that will be used by default. File endings for
+	 * indicating compression will be appended where required.
+	 */
+	public final static String DEFAULT_FILE_NAME = "{PROJECT}-{DATE}.rdf";
+
+	/**
+	 * default action name is used to separate different
+	 * DumpProcessingOutputActions from each other.
+	 */
+	public final static String DEFAULT_ACTION_NAME = "RdfSerializationAction";
+
+	/**
+	 * Internal serializer object that will actually write the RDF output.
+	 */
+	RdfSerializer serializer;
+
+	/**
+	 * A string to identify the overall task to be executed. This is used to
+	 * create the default output file name.
+	 */
+	String taskName = "";
+
+	/**
+	 * Integer that holds the serialization task flags.
+	 */
+	int tasks = 0;
+
+	public RdfSerializationAction() {
+		this.outputDestination = DEFAULT_FILE_NAME;
+	}
+
+	@Override
+	public boolean setOption(String option, String value) {
+		if (super.setOption(option, value)) {
+			return true;
+		}
+
+		switch (option) {
+		case OPTION_RDF_TASKS:
+			setTasks(value);
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	@Override
+	public boolean needsSites() {
+		return this.tasks != 0; // no need for sites when just printing help
+	}
+
+	@Override
+	public boolean isReady() {
+		if (this.tasks == 0) {
+			printHelp();
+			return false;
+		} else {
+			return true;
+		}
+	}
+
+	@Override
+	public void open() {
+		try {
+			this.serializer = createRdfSerializer();
+			this.serializer.open();
+		} catch (IOException e) {
+			// TODO better add proper exceptions to open() declaration
+			throw new RuntimeException(e.getMessage(), e);
+		}
+	}
+
+	@Override
+	public void processItemDocument(ItemDocument itemDocument) {
+		this.serializer.processItemDocument(itemDocument);
+	}
+
+	@Override
+	public void processPropertyDocument(PropertyDocument propertyDocument) {
+		this.serializer.processPropertyDocument(propertyDocument);
+	}
+
+	@Override
+	public void processLexemeDocument(LexemeDocument lexemeDocument) {
+		this.serializer.processLexemeDocument(lexemeDocument);
+	}
+
+	@Override
+	public void processMediaInfoDocument(MediaInfoDocument mediaInfoDocument) {
+		this.serializer.processMediaInfoDocument(mediaInfoDocument);
+	}
+
+	@Override
+	public void close() {
+		this.serializer.close();
+		super.close();
+	}
+
+	/**
+	 * Creates a new RDF serializer based on the current configuration of this
+	 * object.
+	 *
+	 * @return the newly created RDF serializer
+	 * @throws IOException
+	 *             if there were problems opening the output files
+	 */
+	protected RdfSerializer createRdfSerializer() throws IOException {
+
+		String outputDestinationFinal;
+		if (this.outputDestination != null) {
+			outputDestinationFinal = this.outputDestination;
+		} else {
+			outputDestinationFinal = "{PROJECT}" + this.taskName + "{DATE}"
+					+ ".nt";
+		}
+
+		OutputStream exportOutputStream = getOutputStream(this.useStdOut,
+				insertDumpInformation(outputDestinationFinal),
+				this.compressionType);
+
+		RdfSerializer serializer = new RdfSerializer(RDFFormat.NTRIPLES,
+				exportOutputStream, this.sites,
+				PropertyRegister.getWikidataPropertyRegister());
+		serializer.setTasks(this.tasks);
+
+		return serializer;
+	}
+
+	/**
+	 * Sets the RDF serialization tasks based on the given string value.
+	 *
+	 * @param tasks
+	 *            a space-free, comma-separated list of task names
+	 */
+	private void setTasks(String tasks) {
+		for (String task : tasks.split(",")) {
+			if (KNOWN_TASKS.containsKey(task)) {
+				this.tasks |= KNOWN_TASKS.get(task);
+				this.taskName += (this.taskName.isEmpty() ? "" : "-") + task;
+			} else {
+				logger.warn("Unsupported RDF serialization task \"" + task
+						+ "\". Run without specifying any tasks for help.");
+			}
+		}
+	}
+
+	private void printHelp() {
+		List<String> rdfTasks = new ArrayList<>(KNOWN_TASKS.keySet());
+		Collections.sort(rdfTasks);
+		System.out.println("---");
+		System.out
+				.println(WordUtils
+						.wrap("Use option --"
+								+ OPTION_RDF_TASKS
+								+ " to select the supported tasks for RDF export. "
+								+ "Tasks may select which entities to consider, and which data to include.",
+								75)
+						+ "\nExample: \"items,labels,aliases\" exports labels and aliases of items."
+						+ "\n\nAvailable tasks:");
+		for (String supportedTask : rdfTasks) {
+			System.out.println(WordUtils.wrap("* \"" + supportedTask + "\": "
+					+ TASK_HELP.get(supportedTask), 75, "\n   ", true));
+		}
+	}
+
+	@Override
+	public String getReport() {
+		if (this.serializer != null) {
+			String message = "Finished serialization of "
+					+ this.serializer.getTripleCount()
+					+ " RDF triples in file "
+					+ this.insertDumpInformation(this.outputDestination);
+			if (!this.compressionType.equals(COMPRESS_NONE)) {
+				message += "." + this.compressionType;
+			}
+			return message;
+		} else {
+			logger.warn("Could not create report. open() was not executed.");
+			return "";
+		}
+	}
+
+	@Override
+	public String getDefaultActionName() {
+		return DEFAULT_ACTION_NAME;
+	}
+}
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/SchemaUsageAnalyzer.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/SchemaUsageAnalyzer.java
new file mode 100644
index 00000000..0537e04f
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/SchemaUsageAnalyzer.java
@@ -0,0 +1,1006 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Examples
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.wikidata.wdtk.datamodel.helpers.Datamodel;
+import org.wikidata.wdtk.datamodel.interfaces.ItemDocument;
+import org.wikidata.wdtk.datamodel.interfaces.ItemIdValue;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyDocument;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyIdValue;
+import org.wikidata.wdtk.datamodel.interfaces.SiteLink;
+import org.wikidata.wdtk.datamodel.interfaces.Sites;
+import org.wikidata.wdtk.datamodel.interfaces.Snak;
+import org.wikidata.wdtk.datamodel.interfaces.SnakGroup;
+import org.wikidata.wdtk.datamodel.interfaces.Statement;
+import org.wikidata.wdtk.datamodel.interfaces.StatementDocument;
+import org.wikidata.wdtk.datamodel.interfaces.StatementGroup;
+import org.wikidata.wdtk.datamodel.interfaces.StatementRank;
+import org.wikidata.wdtk.datamodel.interfaces.StringValue;
+import org.wikidata.wdtk.datamodel.interfaces.TermedDocument;
+import org.wikidata.wdtk.datamodel.interfaces.Value;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ *
+ * @author Markus Kroetzsch
+ */
+public class SchemaUsageAnalyzer implements DumpProcessingAction {
+
+	private static final ItemIdValue ItemGadgetAuthorityControl = Datamodel
+			.makeWikidataItemIdValue("Q22348290");
+
+	/**
+	 * Object mapper that is used to serialize JSON.
+	 */
+	protected static final ObjectMapper mapper = new ObjectMapper();
+	static {
+		mapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+	}
+
+	/**
+	 * The place where result files will be stored.
+	 */
+	protected Path resultDirectory;
+
+	/**
+	 * Sites information used to extract site data.
+	 */
+	protected Sites sites;
+
+	/**
+	 * Name as an action, if any
+	 */
+	protected String name = null;
+
+	protected String dateStamp;
+	protected String project;
+
+	/**
+	 * Simple record class to keep track of some usage numbers for one type of
+	 * entity.
+	 *
+	 * @author Markus Kroetzsch
+	 *
+	 */
+	class EntityStatistics {
+		@JsonProperty("c")
+		long count = 0;
+		@JsonProperty("cLabels")
+		long countLabels = 0;
+		@JsonProperty("cDesc")
+		long countDescriptions = 0;
+		@JsonProperty("cAliases")
+		long countAliases = 0;
+		@JsonProperty("cStmts")
+		long countStatements = 0;
+		// long countReferencedStatements = 0;
+
+		// Maps to store property usage data for each language:
+		// final HashMap<String, Integer> labelCounts = new HashMap<>();
+		// final HashMap<String, Integer> descriptionCounts = new HashMap<>();
+		// final HashMap<String, Integer> aliasCounts = new HashMap<>();
+	}
+
+	/**
+	 * Class to record the use of some class item or property.
+	 *
+	 * @author Markus Kroetzsch
+	 * @author Markus Damm
+	 *
+	 */
+	private abstract class UsageRecord {
+		/**
+		 * Number of items using this entity. For properties, this is the number
+		 * of items with such a property. For class items, this is the number of
+		 * direct instances of this class.
+		 */
+		@JsonProperty("i")
+		@JsonInclude(Include.NON_EMPTY)
+		public int itemCount = 0;
+		/**
+		 * Map that records how many times certain properties are used on items
+		 * that use this entity (where "use" has the meaning explained for
+		 * {@link UsageRecord#itemCount}).
+		 */
+		@JsonIgnore
+		public HashMap<Integer, Integer> propertyCoCounts = new HashMap<>();
+		/**
+		 * The label of this item. If there isn't any English label available,
+		 * the label is set to null.
+		 */
+		@JsonProperty("l")
+		@JsonInclude(Include.NON_EMPTY)
+		public String label;
+
+		/**
+		 * Returns a list of related properties in a list ordered by a custom
+		 * relatedness measure.
+		 *
+		 * @return
+		 */
+		@JsonProperty("r")
+		@JsonInclude(Include.NON_EMPTY)
+		public Map<String, Integer> getRelatedProperties() {
+			List<ImmutablePair<Integer, Double>> list = new ArrayList<>(
+					this.propertyCoCounts.size());
+			for (Entry<Integer, Integer> coCountEntry : this.propertyCoCounts
+					.entrySet()) {
+				double otherThisItemRate = (double) coCountEntry.getValue()
+						/ this.itemCount;
+				double otherGlobalItemRate = (double) SchemaUsageAnalyzer.this.propertyRecords
+						.get(coCountEntry.getKey()).itemCount
+						/ SchemaUsageAnalyzer.this.countPropertyEntities;
+				double otherThisItemRateStep = 1 / (1 + Math.exp(6 * (-2
+						* otherThisItemRate + 0.5)));
+				double otherInvGlobalItemRateStep = 1 / (1 + Math.exp(6 * (-2
+						* (1 - otherGlobalItemRate) + 0.5)));
+
+				list.add(new ImmutablePair<Integer, Double>(coCountEntry
+						.getKey(), otherThisItemRateStep
+						* otherInvGlobalItemRateStep * otherThisItemRate
+						/ otherGlobalItemRate));
+			}
+
+			Collections.sort(list,
+					new Comparator<ImmutablePair<Integer, Double>>() {
+						@Override
+						public int compare(ImmutablePair<Integer, Double> o1,
+								ImmutablePair<Integer, Double> o2) {
+							return o2.getValue().compareTo(o1.getValue());
+						}
+					});
+
+			LinkedHashMap<String, Integer> result = new LinkedHashMap<>();
+			for (ImmutablePair<Integer, Double> entry : list) {
+				result.put(entry.left.toString(), (int) (10 * entry.right));
+			}
+
+			return result;
+		}
+	}
+
+	/**
+	 * Class to record the usage of a class item in the data.
+	 *
+	 * @author Markus Kroetzsch
+	 */
+	private class ClassRecord extends UsageRecord {
+		/**
+		 * Number of direct subclasses of this class item.
+		 */
+		@JsonProperty("s")
+		@JsonInclude(Include.NON_EMPTY)
+		public int subclassCount = 0;
+		/**
+		 * Number of all (direct and indirect) instances of this class item.
+		 */
+		@JsonProperty("ai")
+		@JsonInclude(Include.NON_EMPTY)
+		public int allInstanceCount = 0;
+		/**
+		 * Number of all (direct and indirect) subclasses of this class item.
+		 */
+		@JsonProperty("as")
+		@JsonInclude(Include.NON_EMPTY)
+		public int allSubclassCount = 0;
+		/**
+		 * List of direct super classes of this class.
+		 */
+		@JsonIgnore
+		public ArrayList<Integer> directSuperClasses = new ArrayList<>();
+		/**
+		 * Set of all super classes of this class.
+		 */
+		@JsonIgnore
+		public Set<Integer> superClasses = new HashSet<>();
+
+		@JsonProperty("sc")
+		@JsonInclude(Include.NON_EMPTY)
+		public String[] getSuperClasses() {
+			String[] result = new String[superClasses.size()];
+			int i = 0;
+			for (Integer id : superClasses) {
+				result[i] = id.toString();
+				i++;
+			}
+			return result;
+		}
+
+		/**
+		 * List of direct subclasses of this class that are included in the
+		 * export. This is only filled at the end of the processing.
+		 */
+		@JsonProperty("sb")
+		@JsonInclude(Include.NON_EMPTY)
+		public ArrayList<String> nonemptyDirectSubclasses = new ArrayList<>();
+	}
+
+	/**
+	 * Class to record the usage of a property in the data.
+	 *
+	 * @author Markus Kroetzsch
+	 */
+	private class PropertyRecord extends UsageRecord {
+
+		/**
+		 * Set of all qualifiers used with this property.
+		 */
+		@JsonIgnore
+		public Map<Integer, Integer> qualifiers = new HashMap<>();
+
+		/**
+		 * Main URL pattern to be used in links, if any.
+		 *
+		 * @return
+		 */
+		@JsonProperty("u")
+		@JsonInclude(Include.NON_EMPTY)
+		public String urlPattern = null;
+
+		/**
+		 * Classes that this property is a direct instance of.
+		 */
+		@JsonIgnore
+		public List<Integer> classes = new ArrayList<>();
+
+		@JsonProperty("pc")
+		@JsonInclude(Include.NON_EMPTY)
+		public List<String> getClasses() {
+			List<String> result = new ArrayList<>();
+			for (Integer i : classes) {
+				result.add(i.toString());
+			}
+			return result;
+		}
+
+		@JsonProperty("qs")
+		@JsonInclude(Include.NON_EMPTY)
+		public Map<String, Integer> getQualifiers() {
+			List<Map.Entry<Integer, Integer>> list = new ArrayList<>(
+					this.qualifiers.entrySet());
+
+			Collections.sort(list,
+					new Comparator<Map.Entry<Integer, Integer>>() {
+						@Override
+						public int compare(Map.Entry<Integer, Integer> o1,
+								Map.Entry<Integer, Integer> o2) {
+							return o2.getValue().compareTo(o1.getValue());
+						}
+					});
+
+			LinkedHashMap<String, Integer> result = new LinkedHashMap<>();
+			for (Map.Entry<Integer, Integer> entry : list) {
+				result.put(entry.getKey().toString(), entry.getValue());
+			}
+
+			return result;
+		}
+
+	}
+
+	private class SiteRecord {
+		@JsonProperty("u")
+		final String urlPattern;
+
+		@JsonProperty("g")
+		final String group;
+
+		@JsonProperty("l")
+		final String language;
+
+		@JsonProperty("i")
+		long itemCount = 0;
+
+		public SiteRecord(String language, String urlPattern, String group) {
+			this.urlPattern = urlPattern;
+			this.group = group;
+			this.language = language;
+		}
+	}
+
+	/**
+	 * Collection of all property records.
+	 */
+	final HashMap<Integer, PropertyRecord> propertyRecords = new HashMap<>();
+	/**
+	 * Collection of all item records of items used as classes.
+	 */
+	final HashMap<Integer, ClassRecord> classRecords = new HashMap<>();
+	/**
+	 * Collection of all site records of items used as classes.
+	 */
+	final HashMap<String, SiteRecord> siteRecords = new HashMap<>();
+
+	/**
+	 * Total number of items processed.
+	 */
+	private long countEntities = 0;
+	/**
+	 * Total number of items that have some statement.
+	 */
+	private long countPropertyEntities = 0;
+	/**
+	 * Total number of site links.
+	 */
+	private long countSiteLinks = 0;
+
+	private final EntityStatistics itemStatistics = new EntityStatistics();
+	private final EntityStatistics propertyStatistics = new EntityStatistics();
+
+	/**
+	 * Create a directory at the given path if it does not exist yet.
+	 *
+	 * @param path
+	 *            the path to the directory
+	 * @throws IOException
+	 *             if it was not possible to create a directory at the given
+	 *             path
+	 */
+	private static void createDirectory(Path path) throws IOException {
+		try {
+			Files.createDirectory(path);
+		} catch (FileAlreadyExistsException e) {
+			if (!Files.isDirectory(path)) {
+				throw e;
+			}
+		}
+	}
+
+	/**
+	 * Opens a new FileOutputStream for a file of the given name in the given
+	 * result directory. Any file of this name that exists already will be
+	 * replaced. The caller is responsible for eventually closing the stream.
+	 *
+	 * @param resultDirectory
+	 *            the path to the result directory
+	 * @param filename
+	 *            the name of the file to write to
+	 * @return FileOutputStream for the file
+	 * @throws IOException
+	 *             if the file or example output directory could not be created
+	 */
+	public static FileOutputStream openResultFileOuputStream(
+			Path resultDirectory, String filename) throws IOException {
+		Path filePath = resultDirectory.resolve(filename);
+		return new FileOutputStream(filePath.toFile());
+	}
+
+	@Override
+	public void processItemDocument(ItemDocument itemDocument) {
+		// Record relevant labels:
+		Integer itemId = getNumId(itemDocument.getEntityId().getId(), false);
+		if (this.classRecords.containsKey(itemId)) {
+			this.classRecords.get(itemId).label = itemDocument.findLabel("en");
+		}
+
+		countTerms(itemDocument, itemStatistics);
+		processStatementDocument(itemDocument, itemStatistics);
+
+		this.countSiteLinks += itemDocument.getSiteLinks().size();
+		for (SiteLink siteLink : itemDocument.getSiteLinks().values()) {
+			countSiteLink(siteLink);
+		}
+	}
+
+	private void countSiteLink(SiteLink siteLink) {
+		if (!this.siteRecords.containsKey(siteLink.getSiteKey())) {
+			String key = siteLink.getSiteKey();
+			String url = this.sites.getPageUrl(key, "$Placeholder12345");
+			if (url == null) {
+				System.err
+						.println("Could not find site information for " + key);
+			} else {
+				url = url.replace("%24Placeholder12345", "$1");
+			}
+			this.siteRecords.put(key,
+					new SiteRecord(this.sites.getLanguageCode(key), url,
+							this.sites.getGroup(key)));
+		}
+		this.siteRecords.get(siteLink.getSiteKey()).itemCount++;
+	}
+
+	@Override
+	public void processPropertyDocument(PropertyDocument propertyDocument) {
+		// Record relevant labels:
+		PropertyRecord pr = getPropertyRecord(propertyDocument.getEntityId());
+		pr.label = propertyDocument.findLabel("en");
+
+		// Find best URL pattern:
+		StatementGroup urlPatterns = propertyDocument
+				.findStatementGroup("P1630");
+		if (urlPatterns != null) {
+			for (Statement s : urlPatterns) {
+				Value v = s.getValue();
+				if (v == null) {
+					continue;
+				}
+				String urlPattern = ((StringValue) v).getString();
+				boolean foundGacUrl = false;
+				if (pr.urlPattern == null) {
+					pr.urlPattern = urlPattern;
+				} else if (!foundGacUrl
+						&& s.getRank() == StatementRank.PREFERRED) {
+					pr.urlPattern = urlPattern;
+				} else if (!foundGacUrl) {
+					Iterator<Snak> snaks = s.getAllQualifiers();
+					while (snaks.hasNext()) {
+						Snak snak = snaks.next();
+						if ("P1535".equals(snak.getPropertyId().getId())
+								&& ItemGadgetAuthorityControl.equals(snak
+										.getValue())) {
+							pr.urlPattern = urlPattern;
+							foundGacUrl = true;
+							break;
+						}
+					}
+				}
+			}
+		}
+
+		// Collect classes that this property is in:
+		StatementGroup instanceClasses = propertyDocument
+				.findStatementGroup("P31");
+		if (instanceClasses != null) {
+			for (Statement s : instanceClasses) {
+				Value v = s.getValue();
+				if (v != null) {
+					pr.classes.add(Integer.parseInt(((ItemIdValue) v).getId()
+							.substring(1)));
+				}
+			}
+		}
+
+		countTerms(propertyDocument, propertyStatistics);
+		processStatementDocument(propertyDocument, propertyStatistics);
+	}
+
+	private void processStatementDocument(StatementDocument statementDocument,
+			EntityStatistics entityStatistics) {
+		this.countEntities++;
+		entityStatistics.count++;
+
+		if (statementDocument.getStatementGroups().size() > 0) {
+			this.countPropertyEntities++;
+		}
+
+		Set<Integer> superClasses = new HashSet<>();
+		StatementGroup instanceOfStatements = statementDocument
+				.findStatementGroup("P31");
+		if (instanceOfStatements != null) {
+			// Compute all superclasses and count direct instances:
+			for (Statement s : instanceOfStatements) {
+				Value v = s.getValue();
+				if (v instanceof ItemIdValue) {
+					Integer vId = getNumId(((ItemIdValue) v).getId(), false);
+					superClasses.add(vId);
+					ClassRecord classRecord = getClassRecord(vId);
+					classRecord.itemCount++;
+					superClasses.addAll(classRecord.superClasses);
+				}
+			}
+
+			// Count item in all superclasses and count cooccuring properties
+			// for all superclasses:
+			for (Integer classId : superClasses) {
+				ClassRecord classRecord = getClassRecord(classId);
+				classRecord.allInstanceCount++;
+				countCooccurringProperties(statementDocument, classRecord, null);
+			}
+		}
+
+		// Count statements:
+		for (StatementGroup sg : statementDocument.getStatementGroups()) {
+			entityStatistics.countStatements += sg.size();
+			PropertyRecord propertyRecord = getPropertyRecord(sg.getProperty());
+			propertyRecord.itemCount++;
+			countCooccurringProperties(statementDocument, propertyRecord,
+					sg.getProperty());
+
+			for (Statement s : sg) {
+				for (SnakGroup snakGroup : s.getQualifiers()) {
+					Integer qualifierId = getNumId(snakGroup.getProperty()
+							.getId(), false);
+					if (propertyRecord.qualifiers.containsKey(qualifierId)) {
+						propertyRecord.qualifiers.put(qualifierId,
+								propertyRecord.qualifiers.get(qualifierId) + 1);
+					} else {
+						propertyRecord.qualifiers.put(qualifierId, 1);
+					}
+				}
+			}
+		}
+
+		// print a report once in a while:
+		if (this.countEntities % 100000 == 0) {
+			printReport();
+			// writeFinalReports(); // DEBUG
+		}
+	}
+
+	/**
+	 * Count the terms (labels, descriptions, aliases) of an item or property
+	 * document.
+	 *
+	 * @param termedDocument
+	 *            document to count the terms of
+	 * @param entityStatistics
+	 *            record where statistics are counted
+	 */
+	protected void countTerms(TermedDocument termedDocument,
+			EntityStatistics entityStatistics) {
+		entityStatistics.countLabels += termedDocument.getLabels().size();
+		// for (MonolingualTextValue mtv : termedDocument.getLabels().values())
+		// {
+		// countKey(usageStatistics.labelCounts, mtv.getLanguageCode(), 1);
+		// }
+
+		entityStatistics.countDescriptions += termedDocument.getDescriptions()
+				.size();
+		// for (MonolingualTextValue mtv : termedDocument.getDescriptions()
+		// .values()) {
+		// countKey(usageStatistics.descriptionCounts, mtv.getLanguageCode(),
+		// 1);
+		// }
+
+		for (String languageKey : termedDocument.getAliases().keySet()) {
+			int count = termedDocument.getAliases().get(languageKey).size();
+			entityStatistics.countAliases += count;
+			// countKey(usageStatistics.aliasCounts, languageKey, count);
+		}
+	}
+
+	/**
+	 * Fetches all subclass relationships (P279) using the SPARQL endpoint, and
+	 * uses them to compute direct and indirect superclasses of each class.
+	 *
+	 * @throws IOException
+	 */
+	private void fetchSubclassHierarchy() throws IOException {
+		System.out.println("Fetching subclass relationships from SPARQL ...");
+		try (InputStream response = runSparqlQuery("PREFIX ps: <http://www.wikidata.org/prop/statement/>\n"
+				+ "PREFIX p: <http://www.wikidata.org/prop/>\n"
+				+ "SELECT ?subC ?supC WHERE { ?subC p:P279/ps:P279 ?supC }")) {
+			System.out.println("Processing subclass relationships ...");
+
+			// DEBUG
+			// BufferedReader br = new BufferedReader(new InputStreamReader(
+			// response));
+			// String read;
+			// while ((read = br.readLine()) != null) {
+			// System.out.println(read);
+			// }
+			// System.out.println("*** done ***");
+
+			ObjectMapper mapper = new ObjectMapper();
+			JsonNode root = mapper.readTree(response);
+			JsonNode bindings = root.path("results").path("bindings");
+			int count = 0;
+			for (JsonNode binding : bindings) {
+				count++;
+				Integer subId = getNumId(binding.path("subC").path("value")
+						.asText(), true);
+				Integer supId = getNumId(binding.path("supC").path("value")
+						.asText(), true);
+				if (supId == 0 || subId == 0) {
+					System.out.println("Ignoring "
+							+ binding.path("subC").path("value").asText()
+							+ " subClassOf "
+							+ binding.path("supC").path("value").asText());
+					continue;
+				}
+				getClassRecord(subId).directSuperClasses.add(supId);
+				ClassRecord superClass = getClassRecord(supId);
+				superClass.subclassCount++;
+				if (count % 10000 == 0) {
+					System.out.print("*");
+				}
+			}
+			System.out.println("Found " + count
+					+ " subclass relationships among "
+					+ this.classRecords.size() + " Wikidata items.");
+
+			System.out.println("Computing indirect subclass relationships ...");
+			for (ClassRecord classRecord : this.classRecords.values()) {
+				for (Integer superClass : classRecord.directSuperClasses) {
+					addSuperClasses(superClass, classRecord);
+				}
+			}
+
+			System.out.println("Computing total subclass counts ...");
+			for (ClassRecord classRecord : this.classRecords.values()) {
+				for (Integer superClass : classRecord.superClasses) {
+					getClassRecord(superClass).allSubclassCount++;
+				}
+			}
+
+			System.out.println("Preprocessing of class hierarchy complete.");
+		}
+	}
+
+	/**
+	 * Recursively add indirect subclasses to a class record.
+	 *
+	 * @param directSuperClass
+	 *            the superclass to add (together with its own superclasses)
+	 * @param subClassRecord
+	 *            the subclass to add to
+	 */
+	private void addSuperClasses(Integer directSuperClass,
+			ClassRecord subClassRecord) {
+		if (subClassRecord.superClasses.contains(directSuperClass)) {
+			return;
+		}
+		subClassRecord.superClasses.add(directSuperClass);
+		ClassRecord superClassRecord = getClassRecord(directSuperClass);
+		if (superClassRecord == null) {
+			return;
+		}
+
+		for (Integer superClass : superClassRecord.directSuperClasses) {
+			addSuperClasses(superClass, subClassRecord);
+		}
+	}
+
+	/**
+	 * Extracts a numeric id from a string, which can be either a Wikidata
+	 * entity URI or a short entity or property id.
+	 *
+	 * @param idString
+	 * @param isUri
+	 * @return numeric id, or 0 if there was an error
+	 */
+	private Integer getNumId(String idString, boolean isUri) {
+		String numString;
+		if (isUri) {
+			if (!idString.startsWith("http://www.wikidata.org/entity/")) {
+				return 0;
+			}
+			numString = idString.substring("http://www.wikidata.org/entity/Q"
+					.length());
+		} else {
+			numString = idString.substring(1);
+		}
+		return Integer.parseInt(numString);
+	}
+
+	/**
+	 * Returns record where statistics about a class should be stored.
+	 *
+	 * @param classId
+	 *            the numeric id of the class to initialize
+	 * @return the class record
+	 */
+	private ClassRecord getClassRecord(Integer classId) {
+		if (!this.classRecords.containsKey(classId)) {
+			ClassRecord classRecord = new ClassRecord();
+			this.classRecords.put(classId, classRecord);
+			return classRecord;
+		} else {
+			return this.classRecords.get(classId);
+		}
+	}
+
+	/**
+	 * Returns record where statistics about a property should be stored.
+	 *
+	 * @param property
+	 *            the property to initialize
+	 * @return the property record
+	 */
+	private PropertyRecord getPropertyRecord(PropertyIdValue property) {
+		Integer id = getNumId(property.getId(), false);
+		if (!this.propertyRecords.containsKey(id)) {
+			PropertyRecord propertyRecord = new PropertyRecord();
+			this.propertyRecords.put(id, propertyRecord);
+			return propertyRecord;
+		} else {
+			return this.propertyRecords.get(id);
+		}
+	}
+
+	/**
+	 * Counts each property for which there is a statement in the given item
+	 * document, ignoring the property thisPropertyIdValue to avoid properties
+	 * counting themselves.
+	 *
+	 * @param statementDocument
+	 * @param usageRecord
+	 * @param thisPropertyIdValue
+	 */
+	private void countCooccurringProperties(
+			StatementDocument statementDocument, UsageRecord usageRecord,
+			PropertyIdValue thisPropertyIdValue) {
+		for (StatementGroup sg : statementDocument.getStatementGroups()) {
+			if (!sg.getProperty().equals(thisPropertyIdValue)) {
+				Integer propertyId = getNumId(sg.getProperty().getId(), false);
+				if (!usageRecord.propertyCoCounts.containsKey(propertyId)) {
+					usageRecord.propertyCoCounts.put(propertyId, 1);
+				} else {
+					usageRecord.propertyCoCounts.put(propertyId,
+							usageRecord.propertyCoCounts.get(propertyId) + 1);
+				}
+			}
+		}
+	}
+
+	/**
+	 * Executes a given SPARQL query and returns a stream with the result in
+	 * JSON format.
+	 *
+	 * @param query
+	 * @return
+	 * @throws IOException
+	 */
+	private InputStream runSparqlQuery(String query) throws IOException {
+		try {
+			String queryString = "query=" + URLEncoder.encode(query, "UTF-8")
+					+ "&format=json";
+			URL url = new URL("https://query.wikidata.org/sparql?"
+					+ queryString);
+			HttpURLConnection connection = (HttpURLConnection) url
+					.openConnection();
+			connection.setRequestMethod("GET");
+
+			return connection.getInputStream();
+		} catch (UnsupportedEncodingException | MalformedURLException e) {
+			throw new RuntimeException(e.getMessage(), e);
+		}
+	}
+
+	/**
+	 * Creates the final file output of the analysis.
+	 */
+	private void writeFinalReports() {
+		System.out.println("Printing data to output files ...");
+		writePropertyData();
+		writeClassData();
+		System.out.println("Finished printing data.");
+	}
+
+	/**
+	 * Writes all data that was collected about properties to a json file.
+	 */
+	private void writePropertyData() {
+		try (PrintStream out = new PrintStream(openResultFileOuputStream(
+				resultDirectory, "properties.json"))) {
+			out.println("{");
+
+			int count = 0;
+			for (Entry<Integer, PropertyRecord> propertyEntry : this.propertyRecords
+					.entrySet()) {
+				if (count > 0) {
+					out.println(",");
+				}
+				out.print("\"" + propertyEntry.getKey() + "\":");
+				mapper.writeValue(out, propertyEntry.getValue());
+				count++;
+			}
+			out.println("\n}");
+
+			System.out.println(" Serialized information for " + count
+					+ " properties.");
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	public void writeStatisticsData(PrintStream out)
+			throws JsonGenerationException, JsonMappingException, IOException {
+		out.println(" \"entityCount\": \"" + this.countEntities + "\",");
+		out.println(" \"siteLinkCount\": \"" + this.countSiteLinks + "\",");
+
+		out.print(" \"propertyStatistics\": ");
+		mapper.writeValue(out, this.propertyStatistics);
+		out.println(",");
+		out.print(" \"itemStatistics\": ");
+		mapper.writeValue(out, this.itemStatistics);
+		out.println(",");
+
+		out.print(" \"sites\": ");
+		mapper.writeValue(out, this.siteRecords);
+		out.println(",");
+
+	}
+
+	/**
+	 * Writes all data that was collected about classes to a json file.
+	 */
+	private void writeClassData() {
+		try (PrintStream out = new PrintStream(openResultFileOuputStream(
+				resultDirectory, "classes.json"))) {
+			out.println("{");
+
+			// Add direct subclass information:
+			for (Entry<Integer, ClassRecord> classEntry : this.classRecords
+					.entrySet()) {
+				if (classEntry.getValue().subclassCount == 0
+						&& classEntry.getValue().itemCount == 0) {
+					continue;
+				}
+				for (Integer superClass : classEntry.getValue().directSuperClasses) {
+					this.classRecords.get(superClass).nonemptyDirectSubclasses
+							.add(classEntry.getKey().toString());
+				}
+			}
+
+			int count = 0;
+			int countNoLabel = 0;
+			for (Entry<Integer, ClassRecord> classEntry : this.classRecords
+					.entrySet()) {
+				if (classEntry.getValue().subclassCount == 0
+						&& classEntry.getValue().itemCount == 0) {
+					continue;
+				}
+
+				if (classEntry.getValue().label == null) {
+					countNoLabel++;
+				}
+
+				if (count > 0) {
+					out.println(",");
+				}
+				out.print("\"" + classEntry.getKey() + "\":");
+				mapper.writeValue(out, classEntry.getValue());
+				count++;
+			}
+			out.println("\n}");
+
+			System.out.println(" Serialized information for " + count
+					+ " class items.");
+			System.out.println(" -- class items with missing label: "
+					+ countNoLabel);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * Prints a report about the statistics gathered so far.
+	 */
+	private void printReport() {
+		System.out.println(getReport());
+	}
+
+	@Override
+	public boolean needsSites() {
+		return true;
+	}
+
+	@Override
+	public boolean isReady() {
+		return true;
+	}
+
+	@Override
+	public String getReport() {
+		return "Processed " + this.countEntities + " entities:\n"
+				+ " * Property documents: " + this.propertyRecords.size()
+				+ "\n" + " * Class documents: " + this.classRecords.size();
+	}
+
+	@Override
+	public void open() {
+		this.resultDirectory = Paths.get("results");
+		try {
+			// Make output directories for results
+			createDirectory(resultDirectory);
+			resultDirectory = resultDirectory.resolve("wikidatawiki-"
+					+ dateStamp);
+			createDirectory(resultDirectory);
+
+			// Initialise processor object
+			fetchSubclassHierarchy();
+		} catch (IOException e) {
+			throw new RuntimeException(e.toString(), e);
+		}
+	}
+
+	@Override
+	public void close() {
+		writeFinalReports();
+		try (PrintStream out = new PrintStream(openResultFileOuputStream(
+				resultDirectory, "statistics.json"))) {
+			out.println("{ ");
+			writeStatisticsData(out);
+			out.println(" \"dumpDate\": \"" + dateStamp + "\"");
+			out.println("}");
+		} catch (IOException e) {
+			throw new RuntimeException(e.toString(), e);
+		}
+	}
+
+	@Override
+	public void setSites(Sites sites) {
+		this.sites = sites;
+	}
+
+	@Override
+	public boolean setOption(String option, String value) {
+		// no options
+		return false;
+	}
+
+	@Override
+	public boolean useStdOut() {
+		return true;
+	}
+
+	@Override
+	public void setDumpInformation(String project, String dateStamp) {
+		this.project = project;
+		this.dateStamp = dateStamp;
+	}
+
+	@Override
+	public void setActionName(String name) {
+		this.name = name;
+	}
+
+	@Override
+	public String getActionName() {
+		if (this.name != null) {
+			return this.name;
+		} else {
+			return getDefaultActionName();
+		}
+	}
+
+	@Override
+	public String getDefaultActionName() {
+		return "SchemaAnalyzerAction";
+	}
+
+}
diff --git a/wdtk-client/src/main/java/org/wikidata/wdtk/client/package-info.java b/wdtk-client/src/main/java/org/wikidata/wdtk/client/package-info.java
new file mode 100644
index 00000000..516df466
--- /dev/null
+++ b/wdtk-client/src/main/java/org/wikidata/wdtk/client/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * Command-line client to download dumps and serialize its data in several formats.
+ * 
+ * @author Michael Günther
+ */
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
diff --git a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupImpl.java b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupImpl.java
index da1f8351..65261aa8 100644
--- a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupImpl.java
+++ b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupImpl.java
@@ -99,7 +99,6 @@ public class StatementGroupImpl extends AbstractList<Statement> implements State
 				bestStatements.add(statement);
 			}
 		}
-		if (bestStatements.size() == 0) return null;
 		return new StatementGroupImpl(bestStatements);
 	}
 
diff --git a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/interfaces/StatementGroup.java b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/interfaces/StatementGroup.java
index efdb9f01..30c0c86e 100644
--- a/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/interfaces/StatementGroup.java
+++ b/wdtk-datamodel/src/main/java/org/wikidata/wdtk/datamodel/interfaces/StatementGroup.java
@@ -45,7 +45,7 @@ public interface StatementGroup extends Collection<Statement> {
 	 * These are the statements with rank {@link StatementRank::PREFERRED }
 	 * if they exists or the one with rank {@link StatementRank::NORMAL }
 	 *
-	 * @return a subset of the current StatementGroup, or null if there are no best statements
+	 * @return a subset of the current StatementGroup
 	 */
 	StatementGroup getBestStatements();
 
diff --git a/wdtk-distribution/pom.xml b/wdtk-distribution/pom.xml
index 4eb1f4c8..c55f4ae2 100644
--- a/wdtk-distribution/pom.xml
+++ b/wdtk-distribution/pom.xml
@@ -80,6 +80,45 @@
 			    </plugins>
 			  </build>
 		</profile>
+		<profile>
+			<id>client</id>
+			<dependencies>
+		  		<dependency>
+					<groupId>${project.groupId}</groupId>
+					<artifactId>wdtk-client</artifactId>
+					<version>${project.version}</version>
+				</dependency>
+			</dependencies>
+			<build>
+			    <plugins>
+			      <plugin>
+			        <groupId>org.apache.maven.plugins</groupId>
+			        <artifactId>maven-shade-plugin</artifactId>
+			        <version>3.2.0</version>
+			        <executions>
+			          <execution>
+			            <phase>package</phase>
+			            <goals>
+			              <goal>shade</goal>
+			            </goals>
+						<configuration>
+							<finalName>wdtk-client-${project.version}</finalName>
+							<!-- Minimization just does not work; removes many relevant classes -->
+							<minimizeJar>false</minimizeJar>
+							<createDependencyReducedPom>false</createDependencyReducedPom>
+			            	<transformers>
+			            		<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+									<mainClass>org.wikidata.wdtk.client.Client</mainClass>
+			            		</transformer>
+								<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+			            	</transformers>
+						</configuration>
+			          </execution>
+			        </executions>
+			      </plugin>
+			    </plugins>
+			  </build>
+		</profile>			
 		<profile>
 			<id>sign</id>
 			<dependencies>
@@ -115,6 +154,12 @@
 					<version>${project.version}</version>
 					<classifier>sources</classifier>
 				</dependency>
+				<dependency>
+					<groupId>${project.groupId}</groupId>
+					<artifactId>wdtk-client</artifactId>
+					<version>${project.version}</version>
+					<classifier>sources</classifier>
+				</dependency>
 				<dependency>
 					<groupId>${project.groupId}</groupId>
 					<artifactId>wdtk-wikibaseapi</artifactId>
diff --git a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/EditOnlineDataExample.java b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/EditOnlineDataExample.java
index 32b44d10..47575e9a 100644
--- a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/EditOnlineDataExample.java
+++ b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/EditOnlineDataExample.java
@@ -129,7 +129,7 @@ public class EditOnlineDataExample {
 		// unique, which would cause problems if this example is run many times.
 
 		ItemDocument newItemDocument = wbde.createItemDocument(itemDocument,
-				"Wikidata Toolkit example test item creation", null);
+				"Wikidata Toolkit example test item creation");
 
 		ItemIdValue newItemId = newItemDocument.getEntityId();
 		System.out.println("*** Successfully created a new item "
@@ -174,7 +174,7 @@ public class EditOnlineDataExample {
 		newItemDocument = wbde.updateStatements(newItemId,
 				Arrays.asList(statement4, statement1WithRef, statement2),
 				Collections.<Statement> emptyList(),
-				"Wikidata Toolkit example test statement addition", null);
+				"Wikidata Toolkit example test statement addition");
 
 		System.out.println("*** Successfully added statements to "
 				+ newItemId.getId()
@@ -203,7 +203,7 @@ public class EditOnlineDataExample {
 
 		newItemDocument = wbde.updateStatements(newItemDocument,
 				Arrays.asList(newStatement1), Arrays.asList(statementToDelete),
-				"Wikidata Toolkit example test statement modification", null);
+				"Wikidata Toolkit example test statement modification");
 
 		System.out.println("*** Successfully updated statements of "
 				+ newItemId.getId()
diff --git a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/RdfSerializationExample.java b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/RdfSerializationExample.java
index dff4324c..a85db6fd 100644
--- a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/RdfSerializationExample.java
+++ b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/RdfSerializationExample.java
@@ -38,6 +38,10 @@ import org.wikidata.wdtk.rdf.RdfSerializer;
 /**
  * This class shows how convert data from wikidata.org to RDF in N-Triples format. The
  * compressed output will be written into an output file.
+ * <p>
+ * The Wikidata Toolkit command line client provides a stand-alone tool for
+ * generating RDF exports and it supports a range of parameters. This example is
+ * merely used to illustrate how to achieve this from your own code if needed.
  *
  * @author Michael Günther
  * @author Markus Kroetzsch
diff --git a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/FixIntegerQuantityPrecisionsBot.java b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/FixIntegerQuantityPrecisionsBot.java
index 6b8da613..cccb0b71 100644
--- a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/FixIntegerQuantityPrecisionsBot.java
+++ b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/FixIntegerQuantityPrecisionsBot.java
@@ -335,7 +335,7 @@ public class FixIntegerQuantityPrecisionsBot implements EntityDocumentProcessor
 			dataEditor.updateStatements(currentItemDocument, updateStatements,
 					Collections.<Statement> emptyList(),
 					"Set exact values for [[Property:" + propertyId + "|"
-							+ propertyId + "]] integer quantities (Task MB2)", null);
+							+ propertyId + "]] integer quantities (Task MB2)");
 
 		} catch (MediaWikiApiErrorException e) {
 			e.printStackTrace();
diff --git a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/SetLabelsForNumbersBot.java b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/SetLabelsForNumbersBot.java
index 886f30b3..b3fd785f 100644
--- a/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/SetLabelsForNumbersBot.java
+++ b/wdtk-examples/src/main/java/org/wikidata/wdtk/examples/bots/SetLabelsForNumbersBot.java
@@ -285,7 +285,7 @@ public class SetLabelsForNumbersBot implements EntityDocumentProcessor {
 					numberString, languages);
 
 			dataEditor.editItemDocument(itemDocumentBuilder.build(), false,
-					"Set labels to numeric value (Task MB1)", null);
+					"Set labels to numeric value (Task MB1)");
 		} catch (MediaWikiApiErrorException e) {
 			e.printStackTrace();
 		} catch (IOException e) {
diff --git a/wdtk-rdf/src/main/java/org/wikidata/wdtk/rdf/PropertyRegister.java b/wdtk-rdf/src/main/java/org/wikidata/wdtk/rdf/PropertyRegister.java
index 72244515..7175243f 100644
--- a/wdtk-rdf/src/main/java/org/wikidata/wdtk/rdf/PropertyRegister.java
+++ b/wdtk-rdf/src/main/java/org/wikidata/wdtk/rdf/PropertyRegister.java
@@ -22,11 +22,7 @@ import java.io.IOException;
  * #L%
  */
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.Map.Entry;
 
 import org.slf4j.Logger;
@@ -105,6 +101,12 @@ public class PropertyRegister {
 	 */
 	int smallestUnfetchedPropertyIdNumber = 1;
 
+	/**
+	 * Properties that are known to be missing. This is used to avoid making
+	 * a request for this property again.
+	 */
+	final Set<String> knownMissing;
+
 	static final PropertyRegister WIKIDATA_PROPERTY_REGISTER = new PropertyRegister(
 			"P1921", ApiConnection.getWikidataApiConnection(),
 			Datamodel.SITE_WIKIDATA);
@@ -127,6 +129,7 @@ public class PropertyRegister {
 			ApiConnection apiConnection, String siteUri) {
 		this.uriPatternPropertyId = uriPatternPropertyId;
 		this.siteUri = siteUri;
+		this.knownMissing = new HashSet<>();
 		dataFetcher = new WikibaseDataFetcher(apiConnection, siteUri);
 	}
 
@@ -312,7 +315,10 @@ public class PropertyRegister {
 		// Don't do anything if all properties up to this index have already
 		// been fetched. In particular, don't try indefinitely to find a
 		// certain property type (maybe the property was deleted).
-		if (this.smallestUnfetchedPropertyIdNumber > propertyIdNumber) {
+		//
+		// If we previously tried to fetch this property and didn't
+		// find it, there is no point in trying again either.
+		if (this.smallestUnfetchedPropertyIdNumber > propertyIdNumber || knownMissing.contains(property.getId())) {
 			return;
 		}
 
@@ -331,11 +337,7 @@ public class PropertyRegister {
 		Map<String, EntityDocument> properties;
 		try {
 			properties = dataFetcher.getEntityDocuments(propertyIds);
-		} catch (MediaWikiApiErrorException e) {
-			logger.error("Error when trying to fetch property data: "
-					+ e.toString());
-			properties = Collections.emptyMap();
-		} catch (IOException e) {
+		} catch (MediaWikiApiErrorException|IOException e) {
 			logger.error("Error when trying to fetch property data: "
 					+ e.toString());
 			properties = Collections.emptyMap();
@@ -353,7 +355,7 @@ public class PropertyRegister {
 			logger.info("Fetched type information for property "
 					+ entry.getKey() + " online: " + datatype);
 
-			if (!DatatypeIdValue.DT_STRING.equals(datatype)) {
+			if (!DatatypeIdValue.DT_STRING.equals(datatype) && !DatatypeIdValue.DT_EXTERNAL_ID.equals(datatype)) {
 				continue;
 			}
 
@@ -380,6 +382,7 @@ public class PropertyRegister {
 		if (!this.datatypes.containsKey(property.getId())) {
 			logger.error("Failed to fetch type information for property "
 					+ property.getId() + " online.");
+			knownMissing.add(property.getId());
 		}
 	}
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/StatementUpdate.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/StatementUpdate.java
index eb4f5b41..13618a6c 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/StatementUpdate.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/StatementUpdate.java
@@ -201,25 +201,11 @@ public class StatementUpdate {
 	 * Performs the update, selecting the appropriate API action depending on
 	 * the nature of the change.
 	 * 
-	 * @param action
-	 *       the endpoint to which the change should be pushed
-	 * @param editAsBot
-	 *        if true, the edit will be flagged as a "bot edit" provided that
-	 *        the logged in user is in the bot group; for regular users, the
-	 *        flag will just be ignored
-	 * @param summary
-	 *        summary for the edit; will be prepended by an automatically
-	 *        generated comment; the length limit of the autocomment
-	 *        together with the summary is 260 characters: everything above
-	 *        that limit will be cut off
-	 * @param tags
-	 *        string identifiers of the tags to apply to the edit.
-	 *        Ignored if null or empty.
 	 * @return the new document after update with the API
 	 * @throws MediaWikiApiErrorException 
 	 * @throws IOException 
 	 */
-	public StatementDocument performEdit(WbEditingAction action, boolean editAsBot, String summary, List<String> tags)
+	public StatementDocument performEdit(WbEditingAction action, boolean editAsBot, String summary)
 			throws IOException, MediaWikiApiErrorException {
 		if (isEmptyEdit()) {
 			return currentDocument;
@@ -235,7 +221,7 @@ public class StatementUpdate {
 			
 			JsonNode response = action.wbSetClaim(
 						JsonSerializer.getJsonString(statement), editAsBot,
-						currentDocument.getRevisionId(), summary, tags);
+						currentDocument.getRevisionId(), summary);
 			
 			StatementImpl.PreStatement preStatement = getDatamodelObjectFromResponse(response, Collections.singletonList("claim"), StatementImpl.PreStatement.class);
 			Statement returnedStatement = preStatement.withSubject(statement.getClaim().getSubject());
@@ -245,7 +231,7 @@ public class StatementUpdate {
 		} else if (!toDelete.isEmpty() && getUpdatedStatements().size() == toDelete.size() && toDelete.size() <= 50) {
 			// we can use "wbremoveclaims" because we are only removing statements
 			
-			JsonNode response = action.wbRemoveClaims(toDelete, editAsBot, currentDocument.getRevisionId(), summary, tags);
+			JsonNode response = action.wbRemoveClaims(toDelete, editAsBot, currentDocument.getRevisionId(), summary);
 			
 			long revisionId = getRevisionIdFromResponse(response);
 			
@@ -254,7 +240,7 @@ public class StatementUpdate {
 			return (StatementDocument) action.wbEditEntity(currentDocument
 				.getEntityId().getId(), null, null, null, getJsonUpdateString(),
 				false, editAsBot, currentDocument
-				.getRevisionId(), summary, tags);
+				.getRevisionId(), summary);
 		}
 	}
 	
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/TermStatementUpdate.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/TermStatementUpdate.java
index 7a4e5e12..e744540e 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/TermStatementUpdate.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/TermStatementUpdate.java
@@ -356,33 +356,18 @@ public class TermStatementUpdate extends StatementUpdate {
 	 * Performs the update, selecting the appropriate API action depending on
 	 * the nature of the change.
 	 * 
-	 * @param action
-	 *       the endpoint to which the change should be pushed
-	 * @param editAsBot
-	 *        if true, the edit will be flagged as a "bot edit" provided that
-	 *        the logged in user is in the bot group; for regular users, the
-	 *        flag will just be ignored
-	 * @param summary
-	 *        summary for the edit; will be prepended by an automatically
-	 *        generated comment; the length limit of the autocomment
-	 *        together with the summary is 260 characters: everything above
-	 *        that limit will be cut off
-	 * @param tags
-	 *        string identifiers of the tags to apply to the edit.
-	 *        Ignored if null or empty.
 	 * @return the new document after update with the API
 	 * @throws MediaWikiApiErrorException 
 	 * @throws IOException 
 	 */
     @Override
-	public TermedStatementDocument performEdit(
-			WbEditingAction action, boolean editAsBot, String summary, List<String> tags)
+	public TermedStatementDocument performEdit(WbEditingAction action, boolean editAsBot, String summary)
 			throws IOException, MediaWikiApiErrorException {
 		Map<String, TermImpl> labelUpdates = getLabelUpdates();
 		Map<String, TermImpl> descriptionUpdates = getDescriptionUpdates();
 		Map<String, List<TermImpl>> aliasUpdates = getAliasUpdates();
 		if (labelUpdates.isEmpty() && descriptionUpdates.isEmpty() && aliasUpdates.isEmpty()) {
-			return (TermedStatementDocument) super.performEdit(action, editAsBot, summary, tags);	
+			return (TermedStatementDocument) super.performEdit(action, editAsBot, summary);	
 		} else {
 			if (super.isEmptyEdit()) {
 				if(labelUpdates.size() == 1
@@ -395,7 +380,7 @@ public class TermStatementUpdate extends StatementUpdate {
 					JsonNode response = action.wbSetLabel(
 							currentDocument.getEntityId().getId(),
 							null, null, null, language, value.getText(), editAsBot,
-							currentDocument.getRevisionId(), summary, tags);
+							currentDocument.getRevisionId(), summary);
 					
 					MonolingualTextValue respondedLabel = getDatamodelObjectFromResponse(response,
 							Arrays.asList("entity","labels",language), TermImpl.class);
@@ -412,7 +397,7 @@ public class TermStatementUpdate extends StatementUpdate {
 					JsonNode response = action.wbSetDescription(
 							currentDocument.getEntityId().getId(),
 							null, null, null, language, value.getText(), editAsBot,
-							currentDocument.getRevisionId(), summary, tags);
+							currentDocument.getRevisionId(), summary);
 					
 					MonolingualTextValue respondedDescription = getDatamodelObjectFromResponse(response,
 							Arrays.asList("entity","descriptions",language), TermImpl.class);
@@ -438,7 +423,7 @@ public class TermStatementUpdate extends StatementUpdate {
 					JsonNode response = action.wbSetAliases(
 							currentDocument.getEntityId().getId(),
 							null, null, null, language, addedStrings, removedStrings, null, editAsBot,
-							currentDocument.getRevisionId(), summary, tags);
+							currentDocument.getRevisionId(), summary);
 					
 					long revisionId = getRevisionIdFromResponse(response);
 
@@ -454,7 +439,7 @@ public class TermStatementUpdate extends StatementUpdate {
 			EntityDocument response = action.wbEditEntity(currentDocument
 				.getEntityId().getId(), null, null, null, getJsonUpdateString(),
 				false, editAsBot, currentDocument
-				.getRevisionId(), summary, tags);
+				.getRevisionId(), summary);
 			return (TermedStatementDocument) response;
     	}
 	}
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbEditingAction.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbEditingAction.java
index 455eea4b..d1ee2a3b 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbEditingAction.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WbEditingAction.java
@@ -44,7 +44,6 @@ import java.util.Map;
  *
  * @author Michael Guenther
  * @author Markus Kroetzsch
- * @author Antonin Delpeuch
  */
 public class WbEditingAction {
 
@@ -265,8 +264,6 @@ public class WbEditingAction {
 	 *            generated comment; the length limit of the autocomment
 	 *            together with the summary is 260 characters: everything above
 	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit. Ignored if null.
 	 * @return the JSON response as returned by the API
 	 * @throws IOException
 	 *             if there was an IO problem. such as missing network
@@ -276,8 +273,8 @@ public class WbEditingAction {
 	 */
 	public EntityDocument wbEditEntity(String id, String site, String title,
 			String newEntity, String data, boolean clear, boolean bot,
-			long baserevid, String summary, List<String> tags)
-					throws IOException, MediaWikiApiErrorException {
+			long baserevid, String summary) throws IOException,
+			MediaWikiApiErrorException {
 
 		Validate.notNull(data,
 				"Data parameter cannot be null when editing entity data");
@@ -289,7 +286,7 @@ public class WbEditingAction {
 		}
 		
 		JsonNode response = performAPIAction("wbeditentity", id, site, title,
-				newEntity, parameters, summary, tags, baserevid, bot);
+				newEntity, parameters, summary, baserevid, bot);
 		return getEntityDocumentFromResponse(response);
 	}
 	
@@ -330,8 +327,6 @@ public class WbEditingAction {
 	 *            generated comment; the length limit of the autocomment
 	 *            together with the summary is 260 characters: everything above
 	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit. Ignored if null.
 	 * @return the label as returned by the API
 	 * @throws IOException
 	 *             if there was an IO problem. such as missing network
@@ -343,7 +338,7 @@ public class WbEditingAction {
 	 */
 	public JsonNode wbSetLabel(String id, String site, String title,
 			String newEntity, String language, String value,
-			boolean bot, long baserevid, String summary, List<String> tags)
+			boolean bot, long baserevid, String summary)
 					throws IOException, MediaWikiApiErrorException {
 		Validate.notNull(language,
 				"Language parameter cannot be null when setting a label");
@@ -355,7 +350,7 @@ public class WbEditingAction {
 		}
 		
 		JsonNode response = performAPIAction("wbsetlabel", id, site, title, newEntity,
-				parameters, summary, tags, baserevid, bot);
+				parameters, summary, baserevid, bot);
 		return response;
 	}
 	
@@ -396,8 +391,6 @@ public class WbEditingAction {
 	 *            generated comment; the length limit of the autocomment
 	 *            together with the summary is 260 characters: everything above
 	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit. Ignored if null.
 	 * @return the JSON response from the API
 	 * @throws IOException
 	 *             if there was an IO problem. such as missing network
@@ -409,8 +402,7 @@ public class WbEditingAction {
 	 */
 	public JsonNode wbSetDescription(String id, String site, String title,
 			String newEntity, String language, String value,
-			boolean bot, long baserevid, String summary,
-			List<String> tags)
+			boolean bot, long baserevid, String summary)
 					throws IOException, MediaWikiApiErrorException {
 		Validate.notNull(language,
 				"Language parameter cannot be null when setting a description");
@@ -422,7 +414,7 @@ public class WbEditingAction {
 		}
 		
 		JsonNode response = performAPIAction("wbsetdescription", id, site, title,
-				newEntity, parameters, summary, tags, baserevid, bot);
+				newEntity, parameters, summary, baserevid, bot);
 		return response;
 	}
 	
@@ -472,8 +464,6 @@ public class WbEditingAction {
 	 *            generated comment; the length limit of the autocomment
 	 *            together with the summary is 260 characters: everything above
 	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit. Ignored if null.
 	 * @return the JSON response from the API
 	 * @throws IOException
 	 *             if there was an IO problem. such as missing network
@@ -486,8 +476,7 @@ public class WbEditingAction {
 	public JsonNode wbSetAliases(String id, String site, String title,
 			String newEntity, String language, List<String> add,
 			List<String> remove, List<String> set,
-			boolean bot, long baserevid, String summary,
-			List<String> tags)
+			boolean bot, long baserevid, String summary)
 					throws IOException, MediaWikiApiErrorException {
 		Validate.notNull(language,
 				"Language parameter cannot be null when setting aliases");
@@ -508,7 +497,7 @@ public class WbEditingAction {
 			parameters.put("remove", ApiConnection.implodeObjects(remove));
 		}
 		
-		JsonNode response = performAPIAction("wbsetaliases", id, site, title, newEntity, parameters, summary, tags, baserevid, bot);
+		JsonNode response = performAPIAction("wbsetaliases", id, site, title, newEntity, parameters, summary, baserevid, bot);
 		return response;
 	}
 	
@@ -533,8 +522,6 @@ public class WbEditingAction {
 	 *            generated comment; the length limit of the autocomment
 	 *            together with the summary is 260 characters: everything above
 	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit. Ignored if null.
 	 * @return the JSON response from the API
 	 * @throws IOException
 	 *             if there was an IO problem. such as missing network
@@ -545,7 +532,7 @@ public class WbEditingAction {
 	 * @throws MediaWikiApiErrorException
 	 */
 	public JsonNode wbSetClaim(String statement,
-			boolean bot, long baserevid, String summary, List<String> tags)
+			boolean bot, long baserevid, String summary)
 					throws IOException, MediaWikiApiErrorException {
 		Validate.notNull(statement,
 				"Statement parameter cannot be null when adding or changing a statement");
@@ -554,7 +541,7 @@ public class WbEditingAction {
 		Map<String, String> parameters = new HashMap<String, String>();
 		parameters.put("claim", statement);
 		
-		return performAPIAction("wbsetclaim", null, null, null, null, parameters, summary, tags, baserevid, bot);
+		return performAPIAction("wbsetclaim", null, null, null, null, parameters, summary, baserevid, bot);
 	}
 	
 	/**
@@ -578,8 +565,6 @@ public class WbEditingAction {
 	 *            generated comment; the length limit of the autocomment
 	 *            together with the summary is 260 characters: everything above
 	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit. Ignored if null.
 	 * @return the JSON response from the API
 	 * @throws IOException
 	 *             if there was an IO problem. such as missing network
@@ -590,7 +575,7 @@ public class WbEditingAction {
 	 * @throws MediaWikiApiErrorException
 	 */
 	public JsonNode wbRemoveClaims(List<String> statementIds,
-			boolean bot, long baserevid, String summary, List<String> tags)
+			boolean bot, long baserevid, String summary)
 					throws IOException, MediaWikiApiErrorException {
 		Validate.notNull(statementIds,
 				"statementIds parameter cannot be null when deleting statements");
@@ -602,7 +587,7 @@ public class WbEditingAction {
 		Map<String, String> parameters = new HashMap<String, String>();
 		parameters.put("claim", String.join("|", statementIds));
 		
-		return performAPIAction("wbremoveclaims", null, null, null, null, parameters, summary, tags, baserevid, bot);
+		return performAPIAction("wbremoveclaims", null, null, null, null, parameters, summary, baserevid, bot);
 	}
 	
 	/**
@@ -647,8 +632,6 @@ public class WbEditingAction {
 	 *            generated comment; the length limit of the autocomment
 	 *            together with the summary is 260 characters: everything above
 	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit. Ignored if null.
 	 * @return the JSON response from the API
 	 * @throws IOException
 	 *             if there was an IO problem. such as missing network
@@ -664,7 +647,6 @@ public class WbEditingAction {
 			String newEntity,
 			Map<String, String> parameters,
 			String summary,
-			List<String> tags,
 			long baserevid,
 			boolean bot)
 			throws IOException, MediaWikiApiErrorException {
@@ -706,10 +688,6 @@ public class WbEditingAction {
 		if (summary != null) {
 			parameters.put("summary", summary);
 		}
-		
-		if (tags != null && !tags.isEmpty()) {
-			parameters.put("tags", String.join("|", tags));
-		}
 
 		parameters.put("maxlag", Integer.toString(this.maxLag));
 		parameters.put("token", connection.getOrFetchToken("csrf"));
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditor.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditor.java
index 6c014ad7..f9fa336b 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditor.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditor.java
@@ -227,13 +227,7 @@ public class WikibaseDataEditor {
 	 * @param itemDocument
 	 *            the document that contains the data to be written
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
-	 *            Ignored if null or empty.
+	 *            additional summary message for the edit, or null to omit this
 	 * @return newly created item document, or null if there was an error
 	 * @throws IOException
 	 *             if there was an IO problem, such as missing network
@@ -241,10 +235,10 @@ public class WikibaseDataEditor {
 	 * @throws MediaWikiApiErrorException
 	 */
 	public ItemDocument createItemDocument(ItemDocument itemDocument,
-			String summary, List<String> tags) throws IOException, MediaWikiApiErrorException {
+			String summary) throws IOException, MediaWikiApiErrorException {
 		String data = JsonSerializer.getJsonString(itemDocument);
 		return (ItemDocument) this.wbEditingAction.wbEditEntity(null, null,
-				null, "item", data, false, this.editAsBot, 0, summary, tags);
+				null, "item", data, false, this.editAsBot, 0, summary);
 	}
 
 	/**
@@ -262,13 +256,7 @@ public class WikibaseDataEditor {
 	 * @param propertyDocument
 	 *            the document that contains the data to be written
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
-	 *            Ignored if null or empty.
+	 *            additional summary message for the edit, or null to omit this
 	 * @return newly created property document, or null if there was an error
 	 * @throws IOException
 	 *             if there was an IO problem, such as missing network
@@ -276,12 +264,12 @@ public class WikibaseDataEditor {
 	 * @throws MediaWikiApiErrorException
 	 */
 	public PropertyDocument createPropertyDocument(
-			PropertyDocument propertyDocument, String summary, List<String> tags)
+			PropertyDocument propertyDocument, String summary)
 			throws IOException, MediaWikiApiErrorException {
 		String data = JsonSerializer.getJsonString(propertyDocument);
 		return (PropertyDocument) this.wbEditingAction
 				.wbEditEntity(null, null, null, "property", data, false,
-						this.editAsBot, 0, summary, tags);
+						this.editAsBot, 0, summary);
 	}
 
 	/**
@@ -313,12 +301,7 @@ public class WikibaseDataEditor {
 	 *            if false, the given data will be added to the existing data,
 	 *            overwriting only parts that are set to new values
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
+	 *            additional summary message for the edit, or null to omit this
 	 * @return the modified item document, or null if there was an error
 	 * @throws IOException
 	 *             if there was an IO problem, such as missing network
@@ -326,12 +309,12 @@ public class WikibaseDataEditor {
 	 * @throws MediaWikiApiErrorException
 	 */
 	public ItemDocument editItemDocument(ItemDocument itemDocument,
-			boolean clear, String summary, List<String> tags) throws IOException,
+			boolean clear, String summary) throws IOException,
 			MediaWikiApiErrorException {
 		String data = JsonSerializer.getJsonString(itemDocument);
 		return (ItemDocument) this.wbEditingAction.wbEditEntity(itemDocument
 				.getEntityId().getId(), null, null, null, data, clear,
-				this.editAsBot, itemDocument.getRevisionId(), summary, tags);
+				this.editAsBot, itemDocument.getRevisionId(), summary);
 	}
 
 	/**
@@ -363,12 +346,7 @@ public class WikibaseDataEditor {
 	 *            if false, the given data will be added to the existing data,
 	 *            overwriting only parts that are set to new values
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
+	 *            additional summary message for the edit, or null to omit this
 	 * @return the modified property document, or null if there was an error
 	 * @throws IOException
 	 *             if there was an IO problem, such as missing network
@@ -376,14 +354,13 @@ public class WikibaseDataEditor {
 	 * @throws MediaWikiApiErrorException
 	 */
 	public PropertyDocument editPropertyDocument(
-			PropertyDocument propertyDocument, boolean clear, String summary,
-			List<String> tags)
+			PropertyDocument propertyDocument, boolean clear, String summary)
 			throws IOException, MediaWikiApiErrorException {
 		String data = JsonSerializer.getJsonString(propertyDocument);
 		return (PropertyDocument) this.wbEditingAction.wbEditEntity(
 				propertyDocument.getEntityId().getId(), null, null, null,
 				data, clear, this.editAsBot, propertyDocument.getRevisionId(),
-				summary, tags);
+				summary);
 	}
 
 	/**
@@ -403,12 +380,7 @@ public class WikibaseDataEditor {
 	 *            deleted if they are present in the current document (in
 	 *            exactly the same form, with the same id)
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
+	 *            short edit summary
 	 * @return the updated document
 	 * @throws MediaWikiApiErrorException
 	 *             if the API returns errors
@@ -417,14 +389,13 @@ public class WikibaseDataEditor {
 	 */
 	public ItemDocument updateStatements(ItemIdValue itemIdValue,
 			List<Statement> addStatements, List<Statement> deleteStatements,
-			String summary, List<String> tags)
-					throws MediaWikiApiErrorException, IOException {
+			String summary) throws MediaWikiApiErrorException, IOException {
 
 		ItemDocument currentDocument = (ItemDocument) this.wikibaseDataFetcher
 				.getEntityDocument(itemIdValue.getId());
 
 		return updateStatements(currentDocument, addStatements,
-				deleteStatements, summary, tags);
+				deleteStatements, summary);
 	}
 	
 	
@@ -456,12 +427,7 @@ public class WikibaseDataEditor {
 	 *          deleted if they are present in the current document (in
 	 *          exactly the same form, with the same id)
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
+	 * 			short edit summary
 	 * @return the updated document
 	 * @throws MediaWikiApiErrorException
 	 * 			if the API returns errors
@@ -475,14 +441,13 @@ public class WikibaseDataEditor {
 			List<MonolingualTextValue> deleteAliases,
 			List<Statement> addStatements,
 			List<Statement> deleteStatements,
-			String summary,
-			List<String> tags) throws MediaWikiApiErrorException, IOException {
+			String summary) throws MediaWikiApiErrorException, IOException {
 		ItemDocument currentDocument = (ItemDocument) this.wikibaseDataFetcher
 				.getEntityDocument(itemIdValue.getId());
 		
 		return updateTermsStatements(currentDocument, addLabels,
 				addDescriptions, addAliases, deleteAliases,
-				addStatements, deleteStatements, summary, tags);
+				addStatements, deleteStatements, summary);
 	}
 
 	/**
@@ -501,12 +466,7 @@ public class WikibaseDataEditor {
 	 *            deleted if they are present in the current document (in
 	 *            exactly the same form, with the same id)
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
+	 *            short edit summary
 	 * @return the updated document
 	 * @throws MediaWikiApiErrorException
 	 *             if the API returns errors
@@ -515,14 +475,13 @@ public class WikibaseDataEditor {
 	 */
 	public PropertyDocument updateStatements(PropertyIdValue propertyIdValue,
 			List<Statement> addStatements, List<Statement> deleteStatements,
-			String summary, List<String> tags)
-					throws MediaWikiApiErrorException, IOException {
+			String summary) throws MediaWikiApiErrorException, IOException {
 
 		PropertyDocument currentDocument = (PropertyDocument) this.wikibaseDataFetcher
 				.getEntityDocument(propertyIdValue.getId());
 
 		return updateStatements(currentDocument, addStatements,
-				deleteStatements, summary, tags);
+				deleteStatements, summary);
 	}
 
 	/**
@@ -549,13 +508,7 @@ public class WikibaseDataEditor {
 	 *            deleted if they are present in the current document (in
 	 *            exactly the same form, with the same id)
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
-	 *            Ignored if null or empty.
+	 *            short edit summary
 	 * @return the updated document
 	 * @throws MediaWikiApiErrorException
 	 *             if the API returns errors
@@ -565,8 +518,7 @@ public class WikibaseDataEditor {
 	@SuppressWarnings("unchecked")
 	public <T extends StatementDocument> T updateStatements(T currentDocument,
 			List<Statement> addStatements, List<Statement> deleteStatements,
-			String summary, List<String> tags)
-					throws MediaWikiApiErrorException, IOException {
+			String summary) throws MediaWikiApiErrorException, IOException {
 
 		StatementUpdate statementUpdate = new StatementUpdate(currentDocument,
 				addStatements, deleteStatements);
@@ -578,7 +530,7 @@ public class WikibaseDataEditor {
 			return (T) this.wbEditingAction.wbEditEntity(currentDocument
 				.getEntityId().getId(), null, null, null, statementUpdate
 				.getJsonUpdateString(), false, this.editAsBot, currentDocument
-				.getRevisionId(), summary, tags);
+				.getRevisionId(), summary);
 		}
 	}
 	
@@ -589,7 +541,7 @@ public class WikibaseDataEditor {
 	 * happen. The references of duplicate statements will be merged. The labels
 	 * and aliases in a given language are kept distinct.
 	 * 
-     * @param currentDocument
+         * @param currentDocument
 	 * 			the document to be updated; needs to have a correct revision id and
 	 * 			entity id
 	 * @param addLabels
@@ -611,13 +563,7 @@ public class WikibaseDataEditor {
 	 *          deleted if they are present in the current document (in
 	 *          exactly the same form, with the same id)
 	 * @param summary
-	 *            summary for the edit; will be prepended by an automatically
-	 *            generated comment; the length limit of the autocomment
-	 *            together with the summary is 260 characters: everything above
-	 *            that limit will be cut off
-	 * @param tags
-	 *            string identifiers of the tags to apply to the edit.
-	 *            Ignored if null or empty.
+	 * 			short edit summary
 	 * @return the updated document
 	 * @throws MediaWikiApiErrorException
 	 * 			if the API returns errors
@@ -631,8 +577,7 @@ public class WikibaseDataEditor {
 			List<MonolingualTextValue> addAliases,
 			List<MonolingualTextValue> deleteAliases,
 			List<Statement> addStatements, List<Statement> deleteStatements,
-			String summary, List<String> tags)
-					throws MediaWikiApiErrorException, IOException {
+			String summary) throws MediaWikiApiErrorException, IOException {
 		
 		TermStatementUpdate termStatementUpdate = new TermStatementUpdate(
 				currentDocument,
@@ -640,7 +585,7 @@ public class WikibaseDataEditor {
 				addLabels, addDescriptions, addAliases, deleteAliases);
 		termStatementUpdate.setGuidGenerator(guidGenerator);
 		
-		return  (T) termStatementUpdate.performEdit(wbEditingAction, editAsBot, summary, tags);
+		return  (T) termStatementUpdate.performEdit(wbEditingAction, editAsBot, summary);
 	}
 	
 	/**
@@ -702,6 +647,6 @@ public class WikibaseDataEditor {
 	    return (T) this.wbEditingAction.wbEditEntity(currentDocument
 				.getEntityId().getId(), null, null, null, statementUpdate
 				.getJsonUpdateString(), false, this.editAsBot, currentDocument
-				.getRevisionId(), null, null);
+				.getRevisionId(), null);
 	}
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java
index f274998e..160557a4 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java
@@ -34,7 +34,6 @@ public class MediaWikiApiErrorHandler {
 	public final static String ERROR_NO_SUCH_ENTITY = "no-such-entity";
 	public final static String ERROR_MAXLAG = "maxlag";
 	public final static String ERROR_ASSERT_USER_FAILED = "assertuserfailed";
-	public final static String ERROR_TAGS_APPLY_NOT_ALLOWED = "tags-apply-not-allowed-one";
 
 	/**
 	 * Creates and throws a suitable {@link MediaWikiApiErrorException} for the
@@ -62,8 +61,6 @@ public class MediaWikiApiErrorHandler {
 			throw new MaxlagErrorException(errorMessage);
 		case ERROR_ASSERT_USER_FAILED:
 			throw new AssertUserFailedException(errorMessage);
-		case ERROR_TAGS_APPLY_NOT_ALLOWED:
-			throw new TagsApplyNotAllowedException(errorMessage);
 		default:
 			throw new MediaWikiApiErrorException(errorCode, errorMessage);
 		}
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TagsApplyNotAllowedException.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TagsApplyNotAllowedException.java
deleted file mode 100644
index 17ad1a21..00000000
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TagsApplyNotAllowedException.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.wikidata.wdtk.wikibaseapi.apierrors;
-
-/*-
- * #%L
- * Wikidata Toolkit Wikibase API
- * %%
- * Copyright (C) 2014 - 2019 Wikidata Toolkit Developers
- * %%
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * 
- *      http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * #L%
- */
-
-/**
- * Error thrown when one of the supplied tags for an edit does not exist
- * or cannot be applied manually.
- * 
- * @author Antonin Delpeuch
- */
-public class TagsApplyNotAllowedException extends MediaWikiApiErrorException {
-	private static final long serialVersionUID = -6500316749253610150L;
-
-	/**
-	 * Creates a new exception.
-	 *
-	 * @param errorMessage
-	 *            the error message reported by MediaWiki, or any other
-	 *            meaningful message for the user
-	 */
-	public TagsApplyNotAllowedException(String errorMessage) {
-		super(MediaWikiApiErrorHandler.ERROR_TAGS_APPLY_NOT_ALLOWED, errorMessage);
-	}
-}


Test patch:
diff --git a/wdtk-client/src/test/java/org/wikidata/wdtk/client/ClientConfigurationTest.java b/wdtk-client/src/test/java/org/wikidata/wdtk/client/ClientConfigurationTest.java
new file mode 100644
index 00000000..ef3100ad
--- /dev/null
+++ b/wdtk-client/src/test/java/org/wikidata/wdtk/client/ClientConfigurationTest.java
@@ -0,0 +1,296 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.Test;
+import org.wikidata.wdtk.datamodel.helpers.Datamodel;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyIdValue;
+import org.wikidata.wdtk.dumpfiles.MwDumpFile;
+import org.wikidata.wdtk.dumpfiles.MwLocalDumpFile;
+import org.wikidata.wdtk.rdf.RdfSerializer;
+import org.wikidata.wdtk.testing.MockDirectoryManager;
+import org.wikidata.wdtk.util.DirectoryManagerFactory;
+
+public class ClientConfigurationTest {
+
+	@Test
+	public void testReadConfigFile() throws IOException {
+		String configFile = "src/test/resources/testConf.ini";
+		String[] args = new String[] { "-c", configFile };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		assertTrue(config.getOfflineMode());
+		assertTrue(config.isQuiet());
+		assertEquals("dumps/wikidata/", config.getDumpDirectoryLocation());
+		assertEquals(Collections.<String> emptySet(),
+				config.getFilterSiteKeys());
+		assertEquals(Collections.singleton(Datamodel
+						.makeWikidataPropertyIdValue("P31")),
+				config.getFilterProperties());
+		Set<String> langFilters = new HashSet<>();
+		langFilters.add("fr");
+		langFilters.add("zh");
+		assertEquals(langFilters, config.getFilterLanguages());
+
+		assertEquals(2, config.getActions().size());
+		assertTrue(config.getActions().get(0) instanceof RdfSerializationAction);
+		assertTrue(config.getActions().get(1) instanceof JsonSerializationAction);
+		RdfSerializationAction rdfAction = (RdfSerializationAction) config
+				.getActions().get(0);
+		JsonSerializationAction jsonAction = (JsonSerializationAction) config
+				.getActions().get(1);
+
+		assertTrue(rdfAction.useStdOut);
+		assertEquals(DumpProcessingOutputAction.COMPRESS_GZIP,
+				rdfAction.compressionType);
+		assertEquals("/tmp/wikidata-items.nt", rdfAction.outputDestination);
+		assertEquals(RdfSerializer.TASK_ITEMS | RdfSerializer.TASK_STATEMENTS
+				| RdfSerializer.TASK_TERMS, rdfAction.tasks);
+
+		assertFalse(jsonAction.useStdOut);
+		assertEquals(DumpProcessingOutputAction.COMPRESS_BZ2,
+				jsonAction.compressionType);
+		assertEquals("/tmp/wikidata-dump.json", jsonAction.outputDestination);
+	}
+
+	@Test
+	public void testReadConfigFile2() throws IOException {
+		String configFile = "src/test/resources/testConf2.ini";
+		String[] args = new String[] { "-c", configFile };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		assertFalse(config.getOfflineMode());
+		assertFalse(config.isQuiet());
+		assertEquals("testfile.json.gz", config.getInputDumpLocation());
+		assertEquals("report.txt", config.getReportFileName());
+		// remaining content was already tested above
+	}
+
+	@Test
+	public void testDefaultArguments() {
+		String[] args = new String[] {};
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertFalse(config.getOfflineMode());
+		assertEquals(null, config.getDumpDirectoryLocation());
+		assertEquals(null, config.getFilterLanguages());
+		assertEquals(null, config.getFilterSiteKeys());
+		assertEquals(null, config.getFilterProperties());
+		assertEquals(null, config.getReportFileName());
+		assertEquals(null, config.getInputDumpLocation());
+		assertEquals(null, config.getLocalDumpFile());
+		assertFalse(config.isQuiet());
+	}
+
+	@Test
+	public void testUnknownAction() {
+		String[] args = new String[] { "-a", "notImplemented" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertEquals(0, config.getActions().size());
+	}
+
+	@Test
+	public void testUnknownArguments() {
+		String[] args = new String[] { "--unknown", "-foo" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertFalse(config.getOfflineMode());
+		assertEquals(null, config.getDumpDirectoryLocation());
+		assertFalse(config.isQuiet());
+	}
+
+	@Test
+	public void testDumpLocationArgumentsShort() {
+		String[] args = new String[] { "-d", "dumps/wikidata/" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertEquals("dumps/wikidata/", config.getDumpDirectoryLocation());
+	}
+
+	@Test
+	public void testDumpLocationArgumentsLong() {
+		String[] args = new String[] { "--dumps", "dumps/wikidata/" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertEquals("dumps/wikidata/", config.getDumpDirectoryLocation());
+	}
+
+	@Test
+	public void testOfflineModeArgumentsShort() {
+		String[] args = new String[] { "-n" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertTrue(config.getOfflineMode());
+	}
+
+	@Test
+	public void testOfflineModeArgumentsLong() {
+		String[] args = new String[] { "--offline" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertTrue(config.getOfflineMode());
+	}
+
+	@Test
+	public void testStdOutOutputArgumentsShort() {
+		String[] args = new String[] { "-a", "json", "-s" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertTrue(config.isQuiet());
+	}
+
+	@Test
+	public void testStdOutOutputArgumentsLong() {
+		String[] args = new String[] { "--action", "json", "--stdout" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertTrue(config.isQuiet());
+	}
+
+	@Test
+	public void testQuietArgumentsShort() {
+		String[] args = new String[] { "-q" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertTrue(config.isQuiet());
+	}
+
+	@Test
+	public void testQuietArgumentsLong() {
+		String[] args = new String[] { "--quiet" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertTrue(config.isQuiet());
+	}
+
+	@Test
+	public void testReportArgumentsShort() {
+		String[] args = new String[] { "-r", "output/report.txt" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertEquals("output/report.txt", config.getReportFileName());
+	}
+
+	@Test
+	public void testReportArgumentsLong() {
+		String[] args = new String[] { "--report", "output/report.txt" };
+		ClientConfiguration config = new ClientConfiguration(args);
+		assertEquals("output/report.txt", config.getReportFileName());
+	}
+
+	@Test
+	public void testLanguageFilterArguments() {
+		String[] args = new String[] { "--fLang", "en,de" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		Set<String> langFilters = new HashSet<>();
+		langFilters.add("en");
+		langFilters.add("de");
+
+		assertEquals(langFilters, config.getFilterLanguages());
+	}
+
+	@Test
+	public void testLanguageFilterArgumentsEmpty() {
+		String[] args = new String[] { "--fLang", "-" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		Set<String> langFilters = new HashSet<>();
+
+		assertEquals(langFilters, config.getFilterLanguages());
+	}
+
+	@Test
+	public void testSiteLinkFilterArguments() {
+		String[] args = new String[] { "--fSite", "fawiki,dewiki" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		Set<String> siteFilters = new HashSet<>();
+		siteFilters.add("fawiki");
+		siteFilters.add("dewiki");
+
+		assertEquals(siteFilters, config.getFilterSiteKeys());
+	}
+
+	@Test
+	public void testSiteLinkFilterArgumentsEmpty() {
+		String[] args = new String[] { "--fSite", "-" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		Set<String> siteFilters = new HashSet<>();
+
+		assertEquals(siteFilters, config.getFilterSiteKeys());
+	}
+
+	@Test
+	public void testPropertyFilterArguments() {
+		String[] args = new String[] { "--fProp", "P100,P31" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		Set<PropertyIdValue> propFilters = new HashSet<>();
+		propFilters.add(Datamodel.makeWikidataPropertyIdValue("P31"));
+		propFilters.add(Datamodel.makeWikidataPropertyIdValue("P100"));
+
+		assertEquals(propFilters, config.getFilterProperties());
+	}
+
+	@Test
+	public void testPropertyFilterArgumentsEmpty() {
+		String[] args = new String[] { "--fProp", "-" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		Set<PropertyIdValue> propFilters = new HashSet<>();
+
+		assertEquals(propFilters, config.getFilterProperties());
+	}
+
+	@Test
+	public void testLocalDumpFileLong() {
+		DirectoryManagerFactory
+				.setDirectoryManagerClass(MockDirectoryManager.class);
+		String[] args = new String[] { "--input", "dumptest.json" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		MwDumpFile df = config.getLocalDumpFile();
+
+		assertEquals("dumptest.json", config.getInputDumpLocation());
+		assertTrue(df instanceof MwLocalDumpFile);
+		MwLocalDumpFile ldf = (MwLocalDumpFile) df;
+
+		assertEquals(Paths.get("dumptest.json").toAbsolutePath(), ldf.getPath());
+	}
+
+	@Test
+	public void testLocalDumpFileShort() {
+		DirectoryManagerFactory
+				.setDirectoryManagerClass(MockDirectoryManager.class);
+		String[] args = new String[] { "-i", "dumptest.json" };
+		ClientConfiguration config = new ClientConfiguration(args);
+
+		MwDumpFile df = config.getLocalDumpFile();
+
+		assertEquals("dumptest.json", config.getInputDumpLocation());
+		assertTrue(df instanceof MwLocalDumpFile);
+		MwLocalDumpFile ldf = (MwLocalDumpFile) df;
+
+		assertEquals(Paths.get("dumptest.json").toAbsolutePath(), ldf.getPath());
+	}
+
+}
diff --git a/wdtk-client/src/test/java/org/wikidata/wdtk/client/ClientTest.java b/wdtk-client/src/test/java/org/wikidata/wdtk/client/ClientTest.java
new file mode 100644
index 00000000..ff0f46c5
--- /dev/null
+++ b/wdtk-client/src/test/java/org/wikidata/wdtk/client/ClientTest.java
@@ -0,0 +1,233 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.io.IOUtils;
+import org.apache.log4j.Level;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.wikidata.wdtk.datamodel.interfaces.Sites;
+import org.wikidata.wdtk.dumpfiles.DumpContentType;
+import org.wikidata.wdtk.dumpfiles.DumpProcessingController;
+import org.wikidata.wdtk.dumpfiles.MwDumpFile;
+import org.wikidata.wdtk.testing.MockDirectoryManager;
+import org.wikidata.wdtk.util.CompressionType;
+import org.wikidata.wdtk.util.DirectoryManagerFactory;
+
+public class ClientTest {
+
+	DumpProcessingController mockDpc;
+
+	@Before
+	public void setup() throws IOException {
+		mockDpc = Mockito.mock(DumpProcessingController.class);
+
+		MwDumpFile mockDump = Mockito.mock(MwDumpFile.class);
+		Mockito.when(mockDump.getProjectName()).thenReturn("wikidata");
+		Mockito.when(mockDump.getDateStamp()).thenReturn("20150303");
+
+		Mockito.when(mockDpc.getMostRecentDump(DumpContentType.JSON))
+				.thenReturn(mockDump);
+
+		Sites mockSites = Mockito.mock(Sites.class);
+		Mockito.when(mockDpc.getSitesInformation()).thenReturn(mockSites);
+	}
+
+	@Test
+	public void testDefaultLoggingConfig() throws ParseException, IOException {
+		String[] args = new String[] {};
+		Client client = new Client(mockDpc, args);
+		client.performActions(); // print help
+
+		assertEquals(Level.INFO, Client.consoleAppender.getThreshold());
+		assertEquals(Level.WARN, Client.errorAppender.getThreshold());
+	}
+
+	@Test
+	public void testQuietStdOutLoggingConfig() throws ParseException,
+			IOException {
+		String[] args = new String[] { "-a", "json", "-s" };
+		new Client(mockDpc, args);
+
+		assertEquals(Level.OFF, Client.consoleAppender.getThreshold());
+		assertEquals(Level.WARN, Client.errorAppender.getThreshold());
+	}
+
+	@Test
+	public void testQuietLoggingConfig() throws ParseException, IOException {
+		String[] TEST_ARGS = new String[] { "-a", "json", "-q" };
+		new Client(mockDpc, TEST_ARGS);
+
+		assertEquals(Level.OFF, Client.consoleAppender.getThreshold());
+		assertEquals(Level.WARN, Client.errorAppender.getThreshold());
+	}
+
+	@Test
+	public void testJsonOutput() {
+		String[] args = { "-a", "json", "-o", "output/wikidata.json" };
+		ClientConfiguration configuration = new ClientConfiguration(args);
+		DumpProcessingAction action = configuration.actions.get(0);
+		action.open();
+		action.close();
+		assertTrue(action
+				.getReport()
+				.matches(
+						"Finished serialization of \\d+ EntityDocuments in file output/wikidata.json"));
+	}
+
+	@Test
+	public void testRdfOutput() {
+		String[] args = { "-a", "rdf", "-o", "output/wikidata.rdf" };
+		ClientConfiguration configuration = new ClientConfiguration(args);
+		DumpProcessingAction action = configuration.actions.get(0);
+		action.open();
+		action.close();
+		assertTrue(action
+				.getReport()
+				.matches(
+						"Finished serialization of \\d+ RDF triples in file output/wikidata.rdf"));
+	}
+
+	public void testNonReadyActionWithDumps() throws ParseException,
+			IOException {
+		String[] args = new String[] { "-a", "rdf", "--dumps", "/tmp" };
+		Client client = new Client(mockDpc, args);
+		client.performActions(); // print help
+
+		Mockito.verify(mockDpc, Mockito.never()).processDump(
+				Mockito.<MwDumpFile> any());
+		Mockito.verify(mockDpc, Mockito.never()).getSitesInformation();
+		Mockito.verify(mockDpc).setDownloadDirectory("/tmp");
+	}
+
+	@Test
+	public void testSitesAction() throws ParseException, IOException {
+		String[] args = new String[] { "-a", "rdf", "--rdftasks",
+				"items,labels" };
+		Client client = new Client(mockDpc, args);
+		client.performActions();
+
+		Mockito.verify(mockDpc).processDump(Mockito.<MwDumpFile> any());
+		Mockito.verify(mockDpc).getSitesInformation();
+	}
+
+	@Test
+	public void testSetDumpsDirectoryException() throws ParseException,
+			IOException {
+		Mockito.doThrow(new IOException("Mock exception for testing."))
+				.when(mockDpc).setDownloadDirectory(Mockito.anyString());
+
+		String[] args = new String[] { "-a", "rdf", "--rdftasks",
+				"items,labels", "--dumps", "/tmp/" };
+		Client client = new Client(mockDpc, args);
+		client.performActions(); // print help
+
+		Mockito.verify(mockDpc, Mockito.never()).processDump(
+				Mockito.<MwDumpFile> any());
+		Mockito.verify(mockDpc, Mockito.never()).getSitesInformation();
+	}
+
+	@Test
+	public void testSitesActionException() throws ParseException, IOException {
+		Mockito.doThrow(new IOException()).when(mockDpc).getSitesInformation();
+
+		String[] args = new String[] { "-a", "rdf", "--rdftasks",
+				"items,labels" };
+		Client client = new Client(mockDpc, args);
+		client.performActions(); // print help
+
+		Mockito.verify(mockDpc, Mockito.never()).processDump(
+				Mockito.<MwDumpFile> any());
+		Mockito.verify(mockDpc).getSitesInformation();
+	}
+
+	@Test
+	public void testNonSitesAction() throws ParseException, IOException {
+		String[] args = new String[] { "-a", "json", "-q" };
+		Client client = new Client(mockDpc, args);
+		client.performActions(); // print help
+
+		Mockito.verify(mockDpc).processDump(Mockito.<MwDumpFile> any());
+		Mockito.verify(mockDpc, Mockito.never()).getSitesInformation();
+	}
+
+	@Test
+	public void testWriteReport() throws IOException {
+		DirectoryManagerFactory
+				.setDirectoryManagerClass(MockDirectoryManager.class);
+
+		MockDirectoryManager mdm = new MockDirectoryManager(
+				Paths.get("/output/"), false);
+
+		String[] args = {"-n", "-a", "rdf", "--rdftasks", "aliases", "-o",
+				"/output/wikidata.rdf", "-r", "/output/report.txt" };
+
+		Client client = new Client(mockDpc, args);
+		DumpProcessingAction action = client.clientConfiguration.actions.get(0);
+		action.open();
+		action.close();
+		client.writeReport();
+		assertTrue(IOUtils
+				.toString(
+						mdm.getInputStreamForFile("report.txt",
+								CompressionType.NONE))
+				.matches(
+						"RdfSerializationAction: Finished serialization of \\d+ RDF triples in file /output/wikidata.rdf"
+								+ System.lineSeparator()));
+
+	}
+
+	@Test
+	public void testInsertDumpInformation() throws IOException {
+		DirectoryManagerFactory
+				.setDirectoryManagerClass(MockDirectoryManager.class);
+
+		MockDirectoryManager mdm = new MockDirectoryManager(
+				Paths.get("/output/"), false);
+
+		String[] args = { "-n", "-a", "rdf", "-o", "/output/wikidata.rdf",
+				"--rdftasks", "aliases", "-r", "/output/report-{DATE}.txt" };
+
+		Client client = new Client(mockDpc, args);
+		client.performActions();
+
+		assertTrue(mdm.hasFile("/output/report-"
+				+ client.clientConfiguration.getDateStamp() + ".txt"));
+	}
+
+	@Test
+	public void testNonExistingLocalDump() {
+		String[] args = { "-f", "./asfjl.json" };
+		Client client = new Client(mockDpc, args);
+		client.performActions();
+
+		Mockito.verify(mockDpc, Mockito.never()).processDump(
+				Mockito.<MwDumpFile> any());
+	}
+}
diff --git a/wdtk-client/src/test/java/org/wikidata/wdtk/client/DumpProcessingOutputActionTest.java b/wdtk-client/src/test/java/org/wikidata/wdtk/client/DumpProcessingOutputActionTest.java
new file mode 100644
index 00000000..ed9ed448
--- /dev/null
+++ b/wdtk-client/src/test/java/org/wikidata/wdtk/client/DumpProcessingOutputActionTest.java
@@ -0,0 +1,97 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 - 2015 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Test for general functionality of the abstract class
+ * {@link DumpProcessingOutputActionTest}. We use
+ * {@link JsonSerializationAction} as an example implementation to do the tests.
+ *
+ * @author Markus Kroetzsch
+ *
+ */
+public class DumpProcessingOutputActionTest {
+
+	public static DumpProcessingOutputAction getActionFromArgs(String[] args) {
+		ClientConfiguration config = new ClientConfiguration(args);
+		return (DumpProcessingOutputAction) config.getActions().get(0);
+	}
+
+	@Test
+	public void testDefaults() {
+		String[] args = new String[] { "-a", "json" };
+		DumpProcessingOutputAction action = getActionFromArgs(args);
+
+		assertEquals(action.compressionType,
+				DumpProcessingOutputAction.COMPRESS_NONE);
+		assertFalse(action.useStdOut);
+	}
+
+	@Test
+	public void testCompressionOutputArgumentsShort() {
+		String[] args = new String[] { "-a", "json", "-z", "bz2" };
+		DumpProcessingOutputAction action = getActionFromArgs(args);
+
+		assertEquals(action.compressionType,
+				DumpProcessingOutputAction.COMPRESS_BZ2);
+	}
+
+	@Test
+	public void testCompressionOutputArgumentsLong() {
+		String[] args = new String[] { "-a", "json", "--compression", "GZ" };
+		DumpProcessingOutputAction action = getActionFromArgs(args);
+
+		assertEquals(action.compressionType,
+				DumpProcessingOutputAction.COMPRESS_GZIP);
+	}
+
+	@Test
+	public void testStdOutOutputArgumentsShort() {
+		String[] args = new String[] { "-a", "json", "-s" };
+		DumpProcessingOutputAction action = getActionFromArgs(args);
+
+		assertTrue(action.useStdOut);
+	}
+
+	@Test
+	public void testStdOutOutputArgumentsLong() {
+		String[] args = new String[] { "-a", "json", "--stdout" };
+		DumpProcessingOutputAction action = getActionFromArgs(args);
+
+		assertTrue(action.useStdOut);
+	}
+
+	@Test
+	public void testInsertDumpInformation() {
+		DumpProcessingOutputAction action = new JsonSerializationAction();
+		action.setDumpInformation("wikidata", "20150131");
+		String result = action
+				.insertDumpInformation("{PROJECT}-{DATE}-dump.json");
+		assertEquals(result, "wikidata-20150131-dump.json");
+	}
+
+}
diff --git a/wdtk-client/src/test/java/org/wikidata/wdtk/client/JsonSerializationActionTest.java b/wdtk-client/src/test/java/org/wikidata/wdtk/client/JsonSerializationActionTest.java
new file mode 100644
index 00000000..2b3ace75
--- /dev/null
+++ b/wdtk-client/src/test/java/org/wikidata/wdtk/client/JsonSerializationActionTest.java
@@ -0,0 +1,235 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+import org.wikidata.wdtk.datamodel.helpers.Datamodel;
+import org.wikidata.wdtk.datamodel.helpers.DatamodelMapper;
+import org.wikidata.wdtk.datamodel.implementation.EntityDocumentImpl;
+import org.wikidata.wdtk.datamodel.interfaces.DatatypeIdValue;
+import org.wikidata.wdtk.datamodel.interfaces.EntityDocument;
+import org.wikidata.wdtk.datamodel.interfaces.ItemDocument;
+import org.wikidata.wdtk.datamodel.interfaces.ItemIdValue;
+import org.wikidata.wdtk.datamodel.interfaces.MonolingualTextValue;
+import org.wikidata.wdtk.datamodel.interfaces.PropertyDocument;
+import org.wikidata.wdtk.datamodel.interfaces.SiteLink;
+import org.wikidata.wdtk.datamodel.interfaces.StatementGroup;
+import org.wikidata.wdtk.testing.MockDirectoryManager;
+import org.wikidata.wdtk.util.CompressionType;
+import org.wikidata.wdtk.util.DirectoryManagerFactory;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+public class JsonSerializationActionTest {
+
+	@Test
+	public void testDefaults() {
+		String[] args = new String[] { "-a", "json" };
+		DumpProcessingOutputAction action = DumpProcessingOutputActionTest
+				.getActionFromArgs(args);
+
+		assertTrue(action instanceof JsonSerializationAction);
+		assertFalse(action.needsSites());
+		assertTrue(action.isReady());
+		assertEquals(action.getActionName(), "JsonSerializationAction");
+	}
+
+	@Test
+	public void testJsonOutput() throws IOException {
+		String[] args = new String[] { "-a", "json", "-o",
+				"/path/to/output.json" };
+
+		DirectoryManagerFactory
+				.setDirectoryManagerClass(MockDirectoryManager.class);
+
+		ClientConfiguration config = new ClientConfiguration(args);
+		JsonSerializationAction jsa = (JsonSerializationAction) config
+				.getActions().get(0);
+
+		ItemIdValue subject1 = Datamodel.makeWikidataItemIdValue("Q42");
+		ItemIdValue subject2 = Datamodel.makeWikidataItemIdValue("Q43");
+		MonolingualTextValue mtv1 = Datamodel.makeMonolingualTextValue("Test1",
+				"en");
+		MonolingualTextValue mtv2 = Datamodel.makeMonolingualTextValue("Test2",
+				"fr");
+
+		ItemDocument id1 = Datamodel.makeItemDocument(subject1,
+				Arrays.asList(mtv1, mtv2), Arrays.asList(mtv1),
+				Collections.<MonolingualTextValue> emptyList(),
+				Collections.<StatementGroup> emptyList(),
+				Collections.<String, SiteLink> emptyMap());
+
+		ItemDocument id2 = Datamodel.makeItemDocument(subject2,
+				Collections.<MonolingualTextValue> emptyList(),
+				Arrays.asList(mtv2),
+				Collections.<MonolingualTextValue> emptyList(),
+				Collections.<StatementGroup> emptyList(),
+				Collections.<String, SiteLink> emptyMap());
+
+		PropertyDocument pd1 = Datamodel
+				.makePropertyDocument(
+						Datamodel.makeWikidataPropertyIdValue("P31"),
+						Arrays.asList(mtv1),
+						Collections.<MonolingualTextValue> emptyList(),
+						Arrays.asList(mtv1),
+						Collections.emptyList(),
+						Datamodel
+								.makeDatatypeIdValue(DatatypeIdValue.DT_MONOLINGUAL_TEXT));
+
+		jsa.open();
+		jsa.processItemDocument(id1);
+		jsa.processPropertyDocument(pd1);
+		jsa.processItemDocument(id2);
+		jsa.close();
+
+		MockDirectoryManager mdm = new MockDirectoryManager(
+				Paths.get("/path/to/"), false);
+
+		ObjectMapper mapper = new DatamodelMapper(Datamodel.SITE_WIKIDATA);
+		ObjectReader documentReader = mapper
+				.readerFor(EntityDocumentImpl.class);
+		MappingIterator<EntityDocument> documentIterator = documentReader
+				.readValues(mdm.getInputStreamForFile("output.json",
+						CompressionType.NONE));
+
+		List<EntityDocument> results = new ArrayList<>();
+		while (documentIterator.hasNextValue()) {
+			EntityDocument document = documentIterator.nextValue();
+			results.add(document);
+		}
+		documentIterator.close();
+
+		assertEquals(3, results.size());
+		assertEquals(id1, results.get(0));
+		assertEquals(pd1, results.get(1));
+		assertEquals(id2, results.get(2));
+
+	}
+
+	@Test
+	public void testJsonGzipOutput() throws IOException {
+		String[] args = new String[] { "-a", "json", "-o",
+				"/path/to/output.json", "-z", "gz" };
+
+		DirectoryManagerFactory
+				.setDirectoryManagerClass(MockDirectoryManager.class);
+
+		ClientConfiguration config = new ClientConfiguration(args);
+		JsonSerializationAction jsa = (JsonSerializationAction) config
+				.getActions().get(0);
+
+		ItemIdValue subject1 = Datamodel.makeWikidataItemIdValue("Q42");
+		MonolingualTextValue mtv1 = Datamodel.makeMonolingualTextValue("Test1",
+				"en");
+		MonolingualTextValue mtv2 = Datamodel.makeMonolingualTextValue("Test2",
+				"fr");
+
+		ItemDocument id1 = Datamodel.makeItemDocument(subject1,
+				Arrays.asList(mtv1, mtv2), Arrays.asList(mtv1),
+				Collections.<MonolingualTextValue> emptyList(),
+				Collections.<StatementGroup> emptyList(),
+				Collections.<String, SiteLink> emptyMap());
+
+		jsa.open();
+		jsa.processItemDocument(id1);
+		jsa.close();
+
+		MockDirectoryManager mdm = new MockDirectoryManager(
+				Paths.get("/path/to/"), false);
+
+		ObjectMapper mapper = new DatamodelMapper(Datamodel.SITE_WIKIDATA);
+		ObjectReader documentReader = mapper.readerFor(EntityDocumentImpl.class);
+		MappingIterator<EntityDocument> documentIterator = documentReader
+				.readValues(mdm.getInputStreamForFile("output.json.gz",
+						CompressionType.GZIP));
+
+		List<EntityDocument> results = new ArrayList<>();
+		while (documentIterator.hasNextValue()) {
+			EntityDocument document = documentIterator.nextValue();
+			results.add(document);
+		}
+		documentIterator.close();
+
+		assertEquals(1, results.size());
+		assertEquals(id1, results.get(0));
+	}
+
+	@Test
+	public void testJsonBz2Output() throws IOException {
+		String[] args = new String[] { "-a", "json", "-o", "output.json", "-z",
+				"bz2" };
+
+		DirectoryManagerFactory
+				.setDirectoryManagerClass(MockDirectoryManager.class);
+
+		ClientConfiguration config = new ClientConfiguration(args);
+		JsonSerializationAction jsa = (JsonSerializationAction) config
+				.getActions().get(0);
+
+		ItemIdValue subject1 = Datamodel.makeWikidataItemIdValue("Q42");
+		MonolingualTextValue mtv1 = Datamodel.makeMonolingualTextValue("Test1",
+				"en");
+		MonolingualTextValue mtv2 = Datamodel.makeMonolingualTextValue("Test2",
+				"fr");
+
+		ItemDocument id1 = Datamodel.makeItemDocument(subject1,
+				Arrays.asList(mtv1, mtv2), Arrays.asList(mtv1),
+				Collections.<MonolingualTextValue> emptyList(),
+				Collections.<StatementGroup> emptyList(),
+				Collections.<String, SiteLink> emptyMap());
+
+		jsa.open();
+		jsa.processItemDocument(id1);
+		jsa.close();
+
+		MockDirectoryManager mdm = new MockDirectoryManager(Paths.get("."),
+				false);
+
+		ObjectMapper mapper = new DatamodelMapper(Datamodel.SITE_WIKIDATA);
+		ObjectReader documentReader = mapper.readerFor(EntityDocumentImpl.class);
+		MappingIterator<EntityDocument> documentIterator = documentReader
+				.readValues(mdm.getInputStreamForFile("output.json.bz2",
+						CompressionType.BZ2));
+
+		List<EntityDocument> results = new ArrayList<>();
+		while (documentIterator.hasNextValue()) {
+			EntityDocument document = documentIterator.nextValue();
+			results.add(document);
+		}
+		documentIterator.close();
+
+		assertEquals(1, results.size());
+		assertEquals(id1, results.get(0));
+	}
+}
diff --git a/wdtk-client/src/test/java/org/wikidata/wdtk/client/RdfSerializationActionTest.java b/wdtk-client/src/test/java/org/wikidata/wdtk/client/RdfSerializationActionTest.java
new file mode 100644
index 00000000..85490233
--- /dev/null
+++ b/wdtk-client/src/test/java/org/wikidata/wdtk/client/RdfSerializationActionTest.java
@@ -0,0 +1,71 @@
+package org.wikidata.wdtk.client;
+
+/*
+ * #%L
+ * Wikidata Toolkit Command-line Tool
+ * %%
+ * Copyright (C) 2014 Wikidata Toolkit Developers
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.wikidata.wdtk.rdf.RdfSerializer;
+
+public class RdfSerializationActionTest {
+
+	@Test
+	public void testDefaults() {
+		String[] args = new String[] { "-a", "rdf", "--rdftasks", "entities" };
+		DumpProcessingOutputAction action = DumpProcessingOutputActionTest
+				.getActionFromArgs(args);
+
+		assertTrue(action instanceof RdfSerializationAction);
+		assertTrue(action.needsSites());
+		assertTrue(action.isReady());
+		assertEquals(action.getActionName(), "RdfSerializationAction");
+	}
+
+	@Test
+	public void testDefaultsNoTasks() {
+		String[] args = new String[] { "-a", "rdf", "--stdout" };
+		DumpProcessingOutputAction action = DumpProcessingOutputActionTest
+				.getActionFromArgs(args);
+		action.open();
+		action.close();
+
+		assertTrue(action instanceof RdfSerializationAction);
+		assertFalse(action.needsSites());
+		assertFalse(action.isReady());
+	}
+
+	@Test
+	public void testSerializerSetup() {
+		String[] args = new String[] { "-a", "rdf", "--stdout", "--rdftasks",
+				"properties,labels" };
+		RdfSerializationAction action = (RdfSerializationAction) DumpProcessingOutputActionTest
+				.getActionFromArgs(args);
+		action.open(); // creates and initializes serializer (prints to stdout)
+		action.close(); // just to test that this causes no exceptions
+
+		assertTrue(action.needsSites());
+		assertEquals(action.serializer.getTasks(),
+				RdfSerializer.TASK_PROPERTIES | RdfSerializer.TASK_LABELS);
+
+	}
+}
diff --git a/wdtk-client/src/test/resources/testConf.ini b/wdtk-client/src/test/resources/testConf.ini
new file mode 100644
index 00000000..5ee152ce
--- /dev/null
+++ b/wdtk-client/src/test/resources/testConf.ini
@@ -0,0 +1,26 @@
+[general]
+offline = true
+dumps = dumps/wikidata/
+unknownGlobalOption = someValue
+quiet = true
+fSite = -
+fProp = P31
+fLang = fr,zh
+
+[rdf-dump]
+action = rdf
+stdout = true
+compression = gz
+rdftasks = items,statements,terms,unknowntask
+output = /tmp/wikidata-items.nt
+
+[json-dump]
+action=json
+stdout=false
+compression=bz2
+output = /tmp/wikidata-dump.json
+unknownParameter = some-value
+
+[unknown-action]
+action = notImplemented
+stdout=true
diff --git a/wdtk-client/src/test/resources/testConf2.ini b/wdtk-client/src/test/resources/testConf2.ini
new file mode 100644
index 00000000..1e23815a
--- /dev/null
+++ b/wdtk-client/src/test/resources/testConf2.ini
@@ -0,0 +1,10 @@
+[general]
+offline = false
+quiet = false
+input = testfile.json.gz
+report = report.txt
+
+[rdf-dump]
+action = rdf
+rdftasks = items,statements,terms,unknowntask
+output = /tmp/wikidata-items.nt
diff --git a/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupTest.java b/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupTest.java
index c2b09a73..96f98f1d 100644
--- a/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupTest.java
+++ b/wdtk-datamodel/src/test/java/org/wikidata/wdtk/datamodel/implementation/StatementGroupTest.java
@@ -44,8 +44,6 @@ public class StatementGroupTest {
 			Collections.emptyList(), Collections.emptyList(), subject);
 	private Statement statementEmptyId = new StatementImpl("", StatementRank.NORMAL, mainSnak,
 			Collections.emptyList(), Collections.emptyList(), subject);
-	private Statement statementDeprecrated = new StatementImpl("DepId", StatementRank.DEPRECATED, mainSnak,
-			Collections.emptyList(), Collections.emptyList(), subject);
 	private StatementGroup sg1 = new StatementGroupImpl(Collections.singletonList(statement1));
 	private StatementGroup sg2 = new StatementGroupImpl(Collections.singletonList(statement1));
 
@@ -81,14 +79,6 @@ public class StatementGroupTest {
 		);
 	}
 
-	@Test
-	public void getBestStatementsEmpty() {
-		assertNull(
-				new StatementGroupImpl(Collections.singletonList(statementDeprecrated)).getBestStatements()
-
-		);
-	}
-
 	@Test
 	public void propertyIsCorrect() {
 		assertEquals(sg1.getProperty(), property);
diff --git a/wdtk-rdf/src/test/java/org/wikidata/wdtk/rdf/PropertyRegisterTest.java b/wdtk-rdf/src/test/java/org/wikidata/wdtk/rdf/PropertyRegisterTest.java
index 2efbec4d..a44a4849 100644
--- a/wdtk-rdf/src/test/java/org/wikidata/wdtk/rdf/PropertyRegisterTest.java
+++ b/wdtk-rdf/src/test/java/org/wikidata/wdtk/rdf/PropertyRegisterTest.java
@@ -25,14 +25,12 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
+import org.hamcrest.core.IsCollectionContaining;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Matchers;
 import org.mockito.Mockito;
 import org.wikidata.wdtk.datamodel.helpers.Datamodel;
 import org.wikidata.wdtk.datamodel.implementation.DataObjectFactoryImpl;
@@ -67,6 +65,8 @@ public class PropertyRegisterTest {
 
 		PropertyIdValue pid434 = dataObjectFactory.getPropertyIdValue("P434",
 				this.siteIri);
+		PropertyIdValue pid508 = dataObjectFactory.getPropertyIdValue("P508",
+				this.siteIri);
 		PropertyIdValue pid23 = dataObjectFactory.getPropertyIdValue("P23",
 				this.siteIri);
 		PropertyIdValue pid1921 = dataObjectFactory.getPropertyIdValue("P1921",
@@ -90,6 +90,18 @@ public class PropertyRegisterTest {
 						Collections.<Reference> emptyList(),
 						StatementRank.NORMAL, "000");
 
+		Statement p1921StatementExternalID = dataObjectFactory
+				.getStatement(
+						pid508,
+						dataObjectFactory
+								.getValueSnak(
+										pid1921,
+										dataObjectFactory
+												.getStringValue("http://purl.org/bncf/tid/$1")),
+						Collections.<SnakGroup> emptyList(),
+						Collections.<Reference> emptyList(),
+						StatementRank.NORMAL, "000");
+
 		mockStatementGroups.add(dataObjectFactory.getStatementGroup(Collections
 				.singletonList(p23Statement)));
 		mockStatementGroups.add(dataObjectFactory.getStatementGroup(Collections
@@ -110,20 +122,25 @@ public class PropertyRegisterTest {
 				Collections.<StatementGroup> emptyList(),
 				dataObjectFactory.getDatatypeIdValue(DatatypeIdValue.DT_ITEM),
 				0));
+		mockResult.put("P508", dataObjectFactory.getPropertyDocument(pid508,
+				Collections.emptyList(),
+				Collections.emptyList(),
+				Collections.emptyList(),
+				Collections.singletonList(dataObjectFactory.getStatementGroup(
+						Collections.singletonList(p1921StatementExternalID)
+				)),
+				dataObjectFactory.getDatatypeIdValue(DatatypeIdValue.DT_EXTERNAL_ID),
+				0));
 
 		this.propertyRegister = new PropertyRegister("P1921",
 				new ApiConnection("http://localhost/"), this.siteIri);
 
 		WikibaseDataFetcher dataFetcher = Mockito
 				.mock(WikibaseDataFetcher.class);
-
-		List<String> propertyIds = new ArrayList<String>();
-		propertyIds.add("P434");
-		for (int i = 1; i < 50; i++) {
-			propertyIds.add("P" + i);
-		}
-		Mockito.when(dataFetcher.getEntityDocuments(propertyIds)).thenReturn(
-				mockResult);
+		Mockito.when(dataFetcher.getEntityDocuments((List<String>)Matchers.argThat(IsCollectionContaining.hasItems("P434"))))
+				.thenReturn(mockResult);
+		Mockito.when(dataFetcher.getEntityDocuments((List<String>)Matchers.argThat(IsCollectionContaining.hasItems("P508"))))
+				.thenReturn(mockResult);
 		Mockito.when(dataFetcher.getFilter()).thenReturn(
 				new DocumentDataFilter());
 		this.propertyRegister.dataFetcher = dataFetcher;
@@ -148,6 +165,14 @@ public class PropertyRegisterTest {
 		assertTrue(this.propertyRegister.datatypes.keySet().contains("P434"));
 	}
 
+	@Test
+	public void testFetchPropertyUriPatternExternalID() {
+		PropertyIdValue pid = this.dataObjectFactory.getPropertyIdValue("P508",
+				this.siteIri);
+		assertEquals("http://purl.org/bncf/tid/$1",
+				this.propertyRegister.getPropertyUriPattern(pid));
+	}
+
 	@Test
 	public void testGetPropertyType() {
 		assertEquals(DatatypeIdValue.DT_STRING,
@@ -165,10 +190,13 @@ public class PropertyRegisterTest {
 	@Test
 	public void testGetMissingPropertyType() {
 		assertNull(this.propertyRegister.getPropertyType(dataObjectFactory
-				.getPropertyIdValue("P10", this.siteIri)));
+				.getPropertyIdValue("P10000", this.siteIri)));
+		final int smallestBefore = this.propertyRegister.smallestUnfetchedPropertyIdNumber;
 		// Check twice to test fast failing on retry
 		assertNull(this.propertyRegister.getPropertyType(dataObjectFactory
-				.getPropertyIdValue("P10", this.siteIri)));
+				.getPropertyIdValue("P10000", this.siteIri)));
+		assertEquals("no requests should be made if the property is known to be missing",
+				smallestBefore, this.propertyRegister.smallestUnfetchedPropertyIdNumber);
 	}
 
 	@Test
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WbEditingActionTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WbEditingActionTest.java
index 2ccbb6b4..af1259d1 100644
--- a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WbEditingActionTest.java
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WbEditingActionTest.java
@@ -46,7 +46,7 @@ public class WbEditingActionTest {
 				Datamodel.SITE_WIKIDATA);
 
 		EntityDocument result = weea.wbEditEntity("Q42", null, null, null,
-				"{}", true, false, 0, null, null);
+				"{}", true, false, 0, null);
 		assertEquals(null, result);
 	}
 
@@ -76,7 +76,7 @@ public class WbEditingActionTest {
 
 		WbEditingAction weea = new WbEditingAction(con,
 				Datamodel.SITE_WIKIDATA);
-		weea.wbEditEntity("Q42", null, null, null, "{}", false, false, 0, null, null);
+		weea.wbEditEntity("Q42", null, null, null, "{}", false, false, 0, null);
 	}
 
 	@Test(expected = TokenErrorException.class)
@@ -104,7 +104,7 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(con,
 				Datamodel.SITE_WIKIDATA);
 
-		weea.wbEditEntity("Q42", null, null, null, "{}", false, false, 0, null, null);
+		weea.wbEditEntity("Q42", null, null, null, "{}", false, false, 0, null);
 	}
 
 	@Test(expected = MaxlagErrorException.class)
@@ -133,7 +133,7 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(con,
 				Datamodel.SITE_WIKIDATA);
 		WbEditingAction.MAXLAG_SLEEP_TIME = 0; // speed up the test ...
-		weea.wbEditEntity("Q42", null, null, null, "{}", false, false, 0, null, null);
+		weea.wbEditEntity("Q42", null, null, null, "{}", false, false, 0, null);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -141,7 +141,7 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(
 				new MockBasicApiConnection(), Datamodel.SITE_WIKIDATA);
 		weea.wbEditEntity("Q1234", "enwiki", null, null, "{}", false, false, 0,
-				null, null);
+				null);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -149,7 +149,7 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(
 				new MockBasicApiConnection(), Datamodel.SITE_WIKIDATA);
 		weea.wbEditEntity("Q1234", null, "Title", null, "{}", false, false, 0,
-				null, null);
+				null);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -158,7 +158,7 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(
 				new MockBasicApiConnection(), Datamodel.SITE_WIKIDATA);
 		weea.wbEditEntity(null, null, "Title", null, "{}", false, false, 0,
-				null, null);
+				null);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -166,7 +166,7 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(
 				new MockBasicApiConnection(), Datamodel.SITE_WIKIDATA);
 		weea.wbEditEntity("Q1234", null, null, "item", "{}", false, false, 0,
-				null, null);
+				null);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -174,7 +174,7 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(
 				new MockBasicApiConnection(), Datamodel.SITE_WIKIDATA);
 		weea.wbEditEntity(null, "enwiki", null, "item", "{}", false, false, 0,
-				null, null);
+				null);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -183,14 +183,14 @@ public class WbEditingActionTest {
 		WbEditingAction weea = new WbEditingAction(
 				new MockBasicApiConnection(), Datamodel.SITE_WIKIDATA);
 		weea.wbEditEntity(null, null, "Title", "item", "{}", false, false, 0,
-				null, null);
+				null);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
 	public void testNoTask() throws IOException, MediaWikiApiErrorException {
 		WbEditingAction weea = new WbEditingAction(
 				new MockBasicApiConnection(), Datamodel.SITE_WIKIDATA);
-		weea.wbEditEntity(null, null, null, null, "{}", false, false, 0, null, null);
+		weea.wbEditEntity(null, null, null, null, "{}", false, false, 0, null);
 	}
 
 }
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditorTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditorTest.java
index 2117f909..50f212f6 100644
--- a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditorTest.java
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/WikibaseDataEditorTest.java
@@ -46,7 +46,6 @@ import org.wikidata.wdtk.datamodel.interfaces.PropertyIdValue;
 import org.wikidata.wdtk.datamodel.interfaces.Statement;
 import org.wikidata.wdtk.util.CompressionType;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
-import org.wikidata.wdtk.wikibaseapi.apierrors.TagsApplyNotAllowedException;
 import org.wikidata.wdtk.wikibaseapi.apierrors.TokenErrorException;
 
 public class WikibaseDataEditorTest {
@@ -114,10 +113,10 @@ public class WikibaseDataEditorTest {
 
 		assertEquals(1, wde.getRemainingEdits());
 		ItemDocument result = wde
-				.createItemDocument(itemDocument, "My summary", null);
+				.createItemDocument(itemDocument, "My summary");
 		assertEquals(expectedResultDocument, result);
 		assertEquals(0, wde.getRemainingEdits());
-		result = wde.createItemDocument(itemDocument, "My summary", null);
+		result = wde.createItemDocument(itemDocument, "My summary");
 		assertEquals(null, result);
 		assertEquals(0, wde.getRemainingEdits());
 	}
@@ -134,7 +133,7 @@ public class WikibaseDataEditorTest {
 		ItemDocument itemDocument = ItemDocumentBuilder.forItemId(
 				ItemIdValue.NULL).build();
 		ItemDocument result = wde
-				.createItemDocument(itemDocument, "My summary", null);
+				.createItemDocument(itemDocument, "My summary");
 
 		assertEquals(null, result);
 		assertEquals(0, wde.getRemainingEdits());
@@ -159,7 +158,6 @@ public class WikibaseDataEditorTest {
 		params.put("summary", "My summary");
 		params.put("new", "item");
 		params.put("token", "42307b93c79b0cb558d2dfb4c3c92e0955e06041+\\");
-		params.put("tags", "my-tag");
 		params.put("format", "json");
 		params.put("maxlag", "5");
 		String data = JsonSerializer.getJsonString(itemDocument);
@@ -167,7 +165,7 @@ public class WikibaseDataEditorTest {
 		con.setWebResource(params, expectedResult);
 
 		ItemDocument result = wde
-				.createItemDocument(itemDocument, "My summary", Collections.singletonList("my-tag"));
+				.createItemDocument(itemDocument, "My summary");
 
 		assertEquals(expectedResultDocument, result);
 		assertEquals(-1, wde.getRemainingEdits());
@@ -194,7 +192,7 @@ public class WikibaseDataEditorTest {
 		this.con.setWebResourceFromPath(params, this.getClass(),
 				"/error-badtoken.json", CompressionType.NONE);
 
-		wde.createItemDocument(itemDocument, "My summary", null);
+		wde.createItemDocument(itemDocument, "My summary");
 	}
 
 	@Test
@@ -224,9 +222,9 @@ public class WikibaseDataEditorTest {
 		con.setWebResource(params, expectedResult);
 
 		// Create item twice
-		wde.createItemDocument(itemDocument, "My summary", null);
+		wde.createItemDocument(itemDocument, "My summary");
 		ItemDocument result = wde
-				.createItemDocument(itemDocument, "My summary", null);
+				.createItemDocument(itemDocument, "My summary");
 
 		assertEquals(expectedResultDocument, result);
 	}
@@ -260,7 +258,7 @@ public class WikibaseDataEditorTest {
 		con.setWebResource(params, expectedResult);
 
 		ItemDocument result = wde
-				.createItemDocument(itemDocument, "My summary", null);
+				.createItemDocument(itemDocument, "My summary");
 
 		assertEquals(expectedResultDocument, result);
 	}
@@ -288,7 +286,7 @@ public class WikibaseDataEditorTest {
 		params.put("data", data);
 		con.setWebResource(params, expectedResult);
 
-		wde.createItemDocument(itemDocument, "My summary", null);
+		wde.createItemDocument(itemDocument, "My summary");
 	}
 
 	@Test(expected = IOException.class)
@@ -312,7 +310,7 @@ public class WikibaseDataEditorTest {
 		params.put("data", data);
 		con.setWebResource(params, expectedResult);
 
-		wde.createItemDocument(itemDocument, "My summary", null);
+		wde.createItemDocument(itemDocument, "My summary");
 	}
 
 	@Test
@@ -345,7 +343,7 @@ public class WikibaseDataEditorTest {
 		con.setWebResource(params, expectedResult);
 
 		PropertyDocument result = wde.createPropertyDocument(propertyDocument,
-				null, null);
+				null);
 
 		assertTrue(wde.editAsBot());
 		assertEquals(expectedResultDocument, result);
@@ -370,7 +368,6 @@ public class WikibaseDataEditorTest {
 		params.put("action", "wbeditentity");
 		params.put("id", "Q1234");
 		params.put("summary", "My summary");
-		params.put("tags", "tag1|tag2");
 		params.put("token", "42307b93c79b0cb558d2dfb4c3c92e0955e06041+\\");
 		params.put("format", "json");
 		params.put("baserevid", "1234");
@@ -380,7 +377,7 @@ public class WikibaseDataEditorTest {
 		con.setWebResource(params, expectedResult);
 
 		ItemDocument result = wde.editItemDocument(itemDocument, false,
-				"My summary", Arrays.asList("tag1", "tag2"));
+				"My summary");
 
 		assertEquals(expectedResultDocument, result);
 	}
@@ -410,8 +407,7 @@ public class WikibaseDataEditorTest {
 				itemDocument,
 				Arrays.asList(s1dup),
 				Arrays.asList(s2),
-				"Doing spurious changes",
-				null);
+				"Doing spurious changes");
 		
 		// no edit was made at all
 		assertEquals(itemDocument, editedItemDocument);
@@ -450,8 +446,7 @@ public class WikibaseDataEditorTest {
 				Arrays.asList(alias),
 				Arrays.asList(s1dup),
 				Arrays.asList(s2),
-				"Doing spurious changes",
-				null);
+				"Doing spurious changes");
 		
 		// no edit was made at all
 		assertEquals(itemDocument, editedItemDocument);
@@ -526,7 +521,7 @@ public class WikibaseDataEditorTest {
 		ItemDocument editedDocument = wde.updateTermsStatements(itemDocument, Arrays.asList(label),
 				Collections.<MonolingualTextValue>emptyList(), Collections.<MonolingualTextValue>emptyList(),
 				Collections.<MonolingualTextValue>emptyList(), Collections.<Statement>emptyList(),
-				Collections.<Statement>emptyList(), "Adding a label", Collections.emptyList());
+				Collections.<Statement>emptyList(), "Adding a label");
 		
 		assertEquals(expectedDocument, editedDocument);
 	}
@@ -566,7 +561,7 @@ public class WikibaseDataEditorTest {
 		ItemDocument editedDocument = wde.updateTermsStatements(itemDocument, Collections.<MonolingualTextValue>emptyList(), 
 				Arrays.asList(description),	Collections.<MonolingualTextValue>emptyList(),
 				Collections.<MonolingualTextValue>emptyList(), Collections.<Statement>emptyList(),
-				Collections.<Statement>emptyList(), "Adding a description", null);
+				Collections.<Statement>emptyList(), "Adding a description");
 		
 		assertEquals(expectedDocument, editedDocument);
 	}
@@ -612,7 +607,7 @@ public class WikibaseDataEditorTest {
 		ItemDocument editedDocument = wde.updateTermsStatements(itemDocument, Collections.<MonolingualTextValue>emptyList(), 
 				Collections.<MonolingualTextValue>emptyList(), Arrays.asList(addedAlias),
 				Arrays.asList(removedAlias), Collections.<Statement>emptyList(),
-				Collections.<Statement>emptyList(), "Changing aliases", null);
+				Collections.<Statement>emptyList(), "Changing aliases");
 		
 		assertEquals(expectedDocument, editedDocument);
 	}
@@ -639,7 +634,6 @@ public class WikibaseDataEditorTest {
 		Map<String, String> params = new HashMap<String, String>();
 		params.put("action", "wbsetclaim");
 		params.put("summary", "Adding a claim");
-		params.put("tags", "statement-creation");
 		params.put("token", "42307b93c79b0cb558d2dfb4c3c92e0955e06041+\\");
 		params.put("format", "json");
 		params.put("baserevid", "1234");
@@ -651,7 +645,7 @@ public class WikibaseDataEditorTest {
 		ItemDocument editedDocument = wde.updateTermsStatements(itemDocument, Collections.<MonolingualTextValue>emptyList(), 
 				Collections.<MonolingualTextValue>emptyList(), Collections.<MonolingualTextValue>emptyList(),
 				Collections.<MonolingualTextValue>emptyList(), Arrays.asList(s1),
-				Collections.<Statement>emptyList(), "Adding a claim", Collections.singletonList("statement-creation"));
+				Collections.<Statement>emptyList(), "Adding a claim");
 		
 		assertEquals(expectedDocument, editedDocument);
 	}
@@ -692,7 +686,7 @@ public class WikibaseDataEditorTest {
 		ItemDocument editedDocument = wde.updateTermsStatements(itemDocument, Collections.<MonolingualTextValue>emptyList(), 
 				Collections.<MonolingualTextValue>emptyList(), Collections.<MonolingualTextValue>emptyList(),
 				Collections.<MonolingualTextValue>emptyList(), Collections.<Statement>emptyList(),
-				Arrays.asList(s1,s2), "Removing claims", null);
+				Arrays.asList(s1,s2), "Removing claims");
 		
 		assertEquals(expectedDocument, editedDocument);
 	}
@@ -729,46 +723,9 @@ public class WikibaseDataEditorTest {
 		con.setWebResource(params, expectedResult);
 
 		PropertyDocument result = wde.editPropertyDocument(itemDocument, true,
-				"My summary", Collections.emptyList());
+				"My summary");
 
 		assertEquals(expectedResultDocument, result);
 	}
-	
-	@Test(expected = TagsApplyNotAllowedException.class)
-	public void testApplyInvalidTag() throws MediaWikiApiErrorException, IOException {
-		WikibaseDataEditor wde = new WikibaseDataEditor(this.con,
-				Datamodel.SITE_WIKIDATA);
-		ItemIdValue id = Datamodel.makeWikidataItemIdValue("Q1234");
-		Statement s1 = StatementBuilder.forSubjectAndProperty(id, P31)
-				.withValue(Q5).withId("ID-s1").build();
-		MonolingualTextValue description = Datamodel.makeMonolingualTextValue("My description", "en");
-		ItemDocument itemDocument = ItemDocumentBuilder.forItemId(id)
-				.withStatement(s1)
-				.withRevisionId(1234)
-				.build();
-
-		Map<String, String> params = new HashMap<String, String>();
-		params.put("action", "wbsetdescription");
-		params.put("id", "Q1234");
-		params.put("summary", "testing tags");
-		params.put("tags", "tag_which_does_not_exist");
-		params.put("token", "42307b93c79b0cb558d2dfb4c3c92e0955e06041+\\");
-		params.put("format", "json");
-		params.put("baserevid", "1234");
-		params.put("maxlag", "5");
-		params.put("language", "en");
-		params.put("value", "My description");
-		String expectedResult = "{\"error\":"
-				+ "{\"code\":\"tags-apply-not-allowed-one\","
-				+ "\"info\":\"The tag \\\"tag_which_does_not_exist\\\" is not allowed to be manually applied.\","
-				+ "\"*\":\"See https://www.wikidata.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes.\"},"
-				+ "\"servedby\":\"mw1276\"}";
-		con.setWebResource(params, expectedResult);
-		
-		wde.updateTermsStatements(itemDocument, Collections.<MonolingualTextValue>emptyList(), 
-				Arrays.asList(description),	Collections.<MonolingualTextValue>emptyList(),
-				Collections.<MonolingualTextValue>emptyList(), Collections.<Statement>emptyList(),
-				Collections.<Statement>emptyList(), "testing tags", Collections.singletonList("tag_which_does_not_exist"));
-	}
 
 }