diff --git a/README.md b/README.md
index f1df27d..53d0b11 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,15 @@
 # midPoint-Grouper_connector
-Two connectors to be maintained: a REST connector and the Groovy scripts for an AMQP "connector"
+This is a connector that can read groups from a Grouper instance using REST calls.
+Currently it supports these searches only:
+- fetching all groups,
+- fetching a group by name,
+- fetching a group by UUID.
+
+When fetching a group, a client can choose whether to get basic group data only (name, UUID, extension) or whether
+to obtain a list of group members as well. 
+
+Besides `search` operation the following ones are supported:
+- `schema`
+- `test`
+
+This connector was tested with Grouper 2.4.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 9916a84..8baa810 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,11 +23,11 @@
         <artifactId>connector-parent</artifactId>
         <groupId>com.evolveum.polygon</groupId>
         <version>1.4.2.14</version>
-        <relativePath></relativePath>
+        <relativePath/>
     </parent>
 
     <artifactId>connector-grouper-rest</artifactId>
-    <version>0.4</version>
+    <version>0.5</version>
     <packaging>jar</packaging>
 
     <name>Grouper REST Connector</name>
@@ -35,6 +35,7 @@
     <properties>
         <connectorPackage>com.evolveum.polygon.connector.grouper.rest</connectorPackage>
         <connectorClass>GrouperRestConnector</connectorClass>
+        <project.source.version>1.8</project.source.version>
     </properties>
 
     <repositories>
@@ -84,7 +85,6 @@
 	        <groupId>com.evolveum.polygon</groupId>
 	        <version>1.4.2.14-SNAPSHOT</version>
 		</dependency>
-
 		<dependency>
 			<groupId>org.apache.httpcomponents</groupId>
 			<artifactId>httpclient</artifactId>
@@ -101,36 +101,5 @@
 			<version>6.8</version>
 			<scope>test</scope>
 		</dependency>
-		<!--dependency>
-			<groupId>com.metaparadigm</groupId>
-			<artifactId>json-rpc</artifactId>
-			<version>1.0</version>
-		</dependency-->
-		<dependency>
-			<groupId>batik</groupId>
-			<artifactId>batik-swing</artifactId>
-			<version>1.6</version>
-		</dependency>
-		<dependency>
-			<groupId>batik</groupId>
-			<artifactId>batik-rasterizer</artifactId>
-			<version>1.6</version>
-		</dependency>
-		<dependency>
-			<groupId>xml-apis</groupId>
-			<artifactId>xml-apis</artifactId>
-			<version>1.3.04</version>
-		</dependency>
-		<dependency>
-			<groupId>xml-apis</groupId>
-			<artifactId>xml-apis-ext</artifactId>
-			<version>1.3.04</version>
-		</dependency>
-		<!--dependency>
-			<groupId>net.tirasa.connid</groupId>
-			<artifactId>connector-framework</artifactId>
-			<version>1.4.3.0-SNAPSHOT</version>
-		</dependency-->
     </dependencies>
-
 </project>
diff --git a/src/main/assembly/connector.xml b/src/main/assembly/connector.xml
index efca6d1..6d149b4 100644
--- a/src/main/assembly/connector.xml
+++ b/src/main/assembly/connector.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
-  ~ Copyright (c) 2010-2014 Evolveum
+  ~ Copyright (c) 2019 Evolveum
   ~
   ~ Licensed under the Apache License, Version 2.0 (the "License");
   ~ you may not use this file except in compliance with the License.
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/AbstractGroupProcessor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/AbstractGroupProcessor.java
deleted file mode 100644
index 52fd5d2..0000000
--- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/AbstractGroupProcessor.java
+++ /dev/null
@@ -1,201 +0,0 @@
-package com.evolveum.polygon.connector.grouper.rest;
-
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URIBuilder;
-import org.identityconnectors.framework.common.objects.*;
-import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
-import org.identityconnectors.framework.common.objects.filter.Filter;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.net.URISyntaxException;
-
-import static com.evolveum.polygon.connector.grouper.rest.Processor.*;
-
-/**
- *
- */
-public abstract class AbstractGroupProcessor {
-
-	protected static final String ATTR_EXTENSION = "extension";
-	protected final Processor processor;
-
-	public AbstractGroupProcessor(Processor processor) {
-		this.processor = processor;
-	}
-
-	void read(Filter filter, ResultsHandler handler, OperationOptions options) {
-		if (filter == null) {
-			getAllGroups(handler, options);
-		} else if (filter instanceof EqualsFilter && ((EqualsFilter) filter).getAttribute() instanceof Name) {
-			Attribute name = ((EqualsFilter) filter).getAttribute();
-			if (name != null) {
-				if (name.getValue() == null || name.getValue().isEmpty()) {
-					throw new IllegalArgumentException("No group name to look for");
-				} else if (name.getValue().size() > 1) {
-					throw new IllegalArgumentException("More than one group name to look for: " + name.getValue());
-				} else {
-					getGroupByName((String) name.getValue().get(0), handler, options);
-				}
-			} else {
-				processor.throwNullAttrException(filter);
-			}
-		} else if (filter instanceof EqualsFilter && ((EqualsFilter) filter).getAttribute() instanceof Uid) {
-			Attribute name = ((EqualsFilter) filter).getAttribute();
-			if (name != null) {
-				if (name.getValue() == null || name.getValue().isEmpty()) {
-					throw new IllegalArgumentException("No group UUID to look for");
-				} else if (name.getValue().size() > 1) {
-					throw new IllegalArgumentException("More than one group UUID to look for: " + name.getValue());
-				} else {
-					getGroupByUuid((String) name.getValue().get(0), handler, options);
-				}
-			} else {
-				processor.throwNullAttrException(filter);
-			}
-		} else {
-			throw new IllegalArgumentException("Unsupported filter: " + filter);
-		}
-	}
-
-	boolean getGroupByUuid(String uuid, ResultsHandler handler, OperationOptions options) {
-		return getGroupByUuid(uuid, handler);
-	}
-
-	void getGroupByName(String name, ResultsHandler handler, OperationOptions options) {
-		getGroupByName(name, handler);
-	}
-
-	abstract void getAllGroups(ResultsHandler handler, OperationOptions options);
-
-	boolean executeFindGroupsResponse(HttpPost request, JSONObject body, ResultsHandler handler) {
-		System.out.println("Request = " + body.toString());
-		JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON);
-		System.out.println("Got response: " + response);
-		processor.checkSuccess(response, "WsFindGroupsResults");
-		JSONArray groups = processor.getArray(response, false, "WsFindGroupsResults", "groupResults");
-		if (groups != null) {
-			for (Object group : groups) {
-				if (!handleGroupJsonObject(group, handler)) {
-					return false;
-				}
-			}
-		}
-		return true;
-	}
-
-	void executeFindGroupsAsMembersResponse(HttpPost request, JSONObject body, ResultsHandler handler) {
-		System.out.println("Request = " + body.toString());
-		JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON);
-		System.out.println("Got response: " + response);
-		processor.checkSuccess(response, WS_GET_MEMBERS_RESULTS);
-		JSONArray groups = processor.getArray(response, WS_GET_MEMBERS_RESULTS, RESULTS, WS_SUBJECTS);
-		for (Object group : groups) {
-			if (!handleGroupAsMemberJsonObject(group, handler)) {
-				return;
-			}
-		}
-	}
-
-	void getGroupByName(String groupName, ResultsHandler handler) {
-		URIBuilder uriBuilder = processor.getURIBuilder()
-				.setPath(URI_BASE_PATH + PATH_GROUPS);
-		try {
-			HttpPost request = new HttpPost(uriBuilder.build());
-			JSONObject body = new JSONObject()
-					.put("WsRestFindGroupsRequest", new JSONObject()
-							.put("wsGroupLookups", new JSONObject[] { new JSONObject()
-									.put("groupName", groupName) }));
-			executeFindGroupsResponse(request, body, handler);
-		} catch (RuntimeException | URISyntaxException e) {
-			throw processor.processException(e, uriBuilder, "Get all groups");
-		}
-	}
-
-	boolean getGroupByUuid(String groupUuid, ResultsHandler handler) {
-		URIBuilder uriBuilder = processor.getURIBuilder()
-				.setPath(URI_BASE_PATH + PATH_GROUPS);
-		try {
-			HttpPost request = new HttpPost(uriBuilder.build());
-			JSONObject body = new JSONObject()
-					.put("WsRestFindGroupsRequest", new JSONObject()
-							.put("wsGroupLookups", new JSONObject[] { new JSONObject()
-									.put("uuid", groupUuid) }));
-			return executeFindGroupsResponse(request, body, handler);
-		} catch (RuntimeException | URISyntaxException e) {
-			throw processor.processException(e, uriBuilder, "Get all groups");
-		}
-	}
-
-	private boolean handleGroupJsonObject(Object group, ResultsHandler handler) {
-		if (group instanceof JSONObject) {
-			JSONObject gObject = (JSONObject) group;
-			String name = processor.getStringOrNull(gObject, "name");
-			if (processor.groupNameMatches(name)) {
-				String extension = processor.getStringOrNull(gObject, "extension");
-				String uuid = processor.getStringOrNull(gObject, "uuid");
-				ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
-				builder.setObjectClass(getObjectClass());
-				builder.setUid(uuid);
-				builder.setName(name);
-				builder.addAttribute(ATTR_EXTENSION, extension);
-				return handler.handle(builder.build());
-			} else {
-				return true;
-			}
-		} else {
-			throw new IllegalStateException("Expected group as JSONObject, got " + group);
-		}
-	}
-
-	private boolean handleGroupAsMemberJsonObject(Object group, ResultsHandler handler) {
-		if (group instanceof JSONObject) {
-			JSONObject gObject = (JSONObject) group;
-			String sourceId = processor.getStringOrNull(gObject, "sourceId");
-			if (sourceId == null || !sourceId.equals(getConfiguration().getGroupSource())) {
-				LOG.info("Skipping non-group member (source={0})", sourceId);
-				return true;
-			}
-			String name = processor.getStringOrNull(gObject, "name");
-			if (processor.groupNameMatches(name)) {
-				String id = processor.getStringOrNull(gObject, "id");
-				ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
-				builder.setObjectClass(getObjectClass());
-				builder.setUid(id);
-				builder.setName(name);
-				return handler.handle(builder.build());
-			} else {
-				return true;
-			}
-		} else {
-			throw new IllegalStateException("Expected group as JSONObject, got " + group);
-		}
-	}
-
-	protected abstract ObjectClass getObjectClass();
-
-	protected GrouperConfiguration getConfiguration() {
-		return processor.configuration;
-	}
-
-	void test() {
-		URIBuilder uriBuilder = processor.getURIBuilder().setPath(URI_BASE_PATH + PATH_GROUPS);
-		try {
-			HttpPost request = new HttpPost(uriBuilder.build());
-			JSONObject body = new JSONObject()
-					.put("WsRestGetMembersRequest", new JSONObject()
-							.put("wsGroupLookups", new JSONObject[] { new JSONObject()
-									.put("groupName", getConfiguration().getSuperGroup()) })
-							.put("includeSubjectDetail", true)
-							.put("memberFilter", "Immediate"));
-			System.out.println("Request = " + body.toString());
-			JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON);
-			System.out.println("Got response: " + response);
-			processor.checkSuccess(response, WS_GET_MEMBERS_RESULTS);
-			JSONArray groups = processor.getArray(response, WS_GET_MEMBERS_RESULTS, RESULTS, WS_SUBJECTS);
-			System.out.println("Super-group members found: " + groups.length());
-		} catch (RuntimeException | URISyntaxException e) {
-			throw processor.processException(e, uriBuilder, "Test");
-		}
-	}
-}
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/AccountProcessor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/AccountProcessor.java
deleted file mode 100644
index fe87bb7..0000000
--- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/AccountProcessor.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- ******************************************************************************
- * Copyright 2017 Evolveum
- * 
- * 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.
- ******************************************************************************/
-package com.evolveum.polygon.connector.grouper.rest;
-
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URIBuilder;
-import org.identityconnectors.framework.common.objects.*;
-import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
-import org.identityconnectors.framework.common.objects.filter.Filter;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Pattern;
-
-import static com.evolveum.polygon.connector.grouper.rest.Processor.*;
-
-/**
- * @author surmanek
- * @author mederly
- *
- */
-class AccountProcessor {
-
-	private final Processor processor;
-
-	private static final String ATTR_GROUP = "group";
-
-	AccountProcessor(Processor processor) {
-		this.processor = processor;
-	}
-
-	ObjectClassInfoBuilder buildSchema() {
-		ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder();
-		AttributeInfoBuilder groups = new AttributeInfoBuilder(ATTR_GROUP, String.class);
-		groups.setMultiValued(true);
-		builder.addAttributeInfo(groups.build());
-		return builder;
-	}
-
-	void read(Filter filter, ResultsHandler handler, OperationOptions options) {
-		if (filter == null) {
-			getAllUsers(handler);
-		} else if (filter instanceof EqualsFilter &&
-				(((EqualsFilter) filter).getAttribute() instanceof Name || ((EqualsFilter) filter).getAttribute() instanceof Uid)) {
-			Attribute name = ((EqualsFilter) filter).getAttribute();
-			if (name != null) {
-				if (name.getValue() == null || name.getValue().isEmpty()) {
-					throw new IllegalArgumentException("No ID to look for");
-				} else if (name.getValue().size() > 1) {
-					throw new IllegalArgumentException("More than one ID to look for: " + name.getValue());
-				} else {
-					getUser((String) name.getValue().get(0), handler);
-				}
-			} else {
-				processor.throwNullAttrException(filter);
-			}
-		} else {
-			throw new IllegalArgumentException("Unsupported filter: " + filter);
-		}
-	}
-
-	private void getAllUsers(ResultsHandler handler) {
-		URIBuilder uriBuilder = processor.getURIBuilder()
-				.setPath(URI_BASE_PATH + PATH_GROUPS);
-		try {
-			HttpPost request = new HttpPost(uriBuilder.build());
-			JSONObject body = new JSONObject()
-					.put(WS_REST_GET_MEMBERS_REQUEST, new JSONObject()
-							.put(WS_GROUP_LOOKUPS, new JSONObject[] {
-									new JSONObject().put(GROUP_NAME, getConfiguration().getSuperGroup()) }));
-			System.out.println("Request = " + body.toString());
-			JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON);
-			System.out.println("Got response: " + response);
-			processor.checkSuccess(response, WS_GET_MEMBERS_RESULTS);
-			JSONArray subjects = processor.getArray(response, WS_GET_MEMBERS_RESULTS, RESULTS, WS_SUBJECTS);
-			List<String> ids = selectSubjectIds(subjects);
-			System.out.println("Subject IDs found: " + ids);
-			for (String id : ids) {
-				ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
-				builder.setUid(id);
-				builder.setName(id);
-				AttributeBuilder groupBuilder = new AttributeBuilder().setName(ATTR_GROUP);
-				groupBuilder.setAttributeValueCompleteness(AttributeValueCompleteness.INCOMPLETE);
-				builder.addAttribute(groupBuilder.build());
-				if (!handler.handle(builder.build())) {
-					return;
-				}
-			}
-		} catch (RuntimeException | URISyntaxException e) {
-			throw processor.processException(e, uriBuilder, "Get all users");
-		}
-	}
-
-	private List<String> selectSubjectIds(JSONArray subjects) {
-		List<String> rv = new ArrayList<>(subjects.length());
-		for (Object subject : subjects) {
-			if (subject instanceof JSONObject) {
-				JSONObject subjObject = (JSONObject) subject;
-				if (processor.isSuccess(subjObject)) {
-					String sourceId = processor.getStringOrNull(subjObject, "sourceId");
-					if (getConfiguration().getSubjectSource().equals(sourceId)) {
-						rv.add(processor.getString(subjObject, "id"));
-					}
-				} else {
-					LOG.warn("Skipping not-success subject from response: {0}", subject);
-				}
-			} else {
-				throw new IllegalStateException("Expected subject as JSONObject, got " + subject);
-			}
-		}
-		return rv;
-	}
-
-	private void getUser(String id, ResultsHandler handler) {
-		URIBuilder uriBuilder = processor.getURIBuilder()
-				.setPath(URI_BASE_PATH + PATH_SUBJECTS);
-		try {
-			HttpPost request = new HttpPost(uriBuilder.build());
-			JSONObject body = new JSONObject()
-					.put("WsRestGetGroupsRequest", new JSONObject()
-							.put("subjectLookups", new JSONObject[] {
-									new JSONObject().put("subjectId", id) }));
-			System.out.println("Request = " + body.toString());
-			JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON);
-			System.out.println("Got response: " + response);
-			processor.checkSuccess(response, WS_GET_GROUPS_RESULTS);
-			JSONArray groups = processor.getArray(response, WS_GET_GROUPS_RESULTS, RESULTS, WS_GROUPS);
-
-			ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
-			builder.setUid(id);
-			builder.setName(id);
-			builder.addAttribute(ATTR_GROUP, selectGroupNames(groups));
-			handler.handle(builder.build());
-		} catch (RuntimeException | URISyntaxException e) {
-			throw processor.processException(e, uriBuilder, "Get all users");
-		}
-	}
-
-	private List<String> selectGroupNames(JSONArray groups) {
-		List<String> rv = new ArrayList<>();
-		for (Object group : groups) {
-			if (group instanceof JSONObject) {
-				JSONObject gObject = (JSONObject) group;
-				String name = processor.getStringOrNull(gObject, "name");
-				if (processor.groupNameMatches(name)) {
-					rv.add(name);
-				}
-			} else {
-				throw new IllegalStateException("Expected group as JSONObject, got " + group);
-			}
-		}
-		return rv;
-	}
-
-	private GrouperConfiguration getConfiguration() {
-		return processor.configuration;
-	}
-}
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java
new file mode 100644
index 0000000..d085482
--- /dev/null
+++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (c) 2019 Evolveum
+ *
+ * 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.
+ */
+package com.evolveum.polygon.connector.grouper.rest;
+
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.utils.URIBuilder;
+import org.identityconnectors.framework.common.exceptions.ConnectorException;
+import org.identityconnectors.framework.common.objects.*;
+import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Contains logic for handling operations on Group object class.
+ */
+public class GroupProcessor extends Processor {
+
+	public static final String OBJECT_CLASS_NAME = "Group";
+	public static final String ATTR_NAME = "name";
+	public static final String ATTR_UUID = "uuid";
+	private static final String ATTR_EXTENSION = J_EXTENSION;
+	public static final String ATTR_MEMBER = "member";
+
+	private static final String DEFAULT_EXPORT_STEM = ":";
+
+	GroupProcessor(GrouperConfiguration configuration) {
+		super(configuration);
+	}
+
+	ObjectClass getObjectClass() {
+		return new ObjectClass(OBJECT_CLASS_NAME);
+	}
+
+	void read(Filter filter, ResultsHandler handler, OperationOptions options) {
+		if (filter == null) {
+			getAllGroups(handler, options);
+		} else if (filter instanceof EqualsFilter) {
+			Attribute attribute = ((EqualsFilter) filter).getAttribute();
+			if (attribute != null) {
+				List<Object> values = attribute.getValue();
+				if (values == null || values.isEmpty()) {
+					throw new IllegalArgumentException("No attribute value to look for: " + attribute);
+				} else if (values.size() > 1) {
+					throw new IllegalArgumentException("More than one attribute value to look for: " + attribute);
+				}
+				if (attribute.is(Name.NAME) || attribute.is(ATTR_NAME)) {
+					getGroupByName((String) values.get(0), handler, options);
+				} else if (attribute.is(Uid.NAME) || attribute.is(ATTR_UUID)) {
+					getGroupByUuid((String) values.get(0), handler, options);
+				} else {
+					throw new IllegalArgumentException("Equal filter used on unsupported attribute: " + attribute);
+				}
+			} else {
+				throw new IllegalArgumentException("Equal filter used with no attribute: " + filter);
+			}
+		} else {
+			throw new IllegalArgumentException("Unsupported filter: " + filter);
+		}
+	}
+
+	private void getGroupByName(String name, ResultsHandler handler, OperationOptions options) {
+		URIBuilder uriBuilder = getUriBuilderForGroups();
+		if (!isGetMembers(options)) {
+			try {
+				HttpPost request = new HttpPost(uriBuilder.build());
+				JSONObject body = new JSONObject()
+						.put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject()
+								.put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject()
+										.put(J_GROUP_NAME, name) }));
+				executeFindGroups(request, body, handler);
+			} catch (RuntimeException | URISyntaxException e) {
+				throw processException(e, uriBuilder, "Get group by name (no members)");
+			}
+		} else {
+			try {
+				HttpPost request = new HttpPost(uriBuilder.build());
+				JSONObject body = new JSONObject()
+						.put(J_WS_REST_GET_MEMBERS_REQUEST, new JSONObject()
+								.put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject()
+										.put(J_GROUP_NAME, name) })
+								.put(J_INCLUDE_SUBJECT_DETAIL, true));
+				executeGetMembers(request, body, handler);
+			} catch (RuntimeException | URISyntaxException e) {
+				throw processException(e, uriBuilder, "Get group by name (with members)");
+			}
+		}
+	}
+
+	private boolean getGroupByUuid(String uuid, ResultsHandler handler, OperationOptions options) {
+		if (!isGetMembers(options)) {
+			URIBuilder uriBuilder = getUriBuilderForGroups();
+			try {
+				HttpPost request = new HttpPost(uriBuilder.build());
+				JSONObject body = new JSONObject()
+						.put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject()
+								.put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject()
+										.put(J_UUID, uuid) }));
+				return executeFindGroups(request, body, handler);
+			} catch (RuntimeException | URISyntaxException e) {
+				throw processException(e, uriBuilder, "Get group by UUID (no members)");
+			}
+		} else {
+			URIBuilder uriBuilder = getUriBuilderForGroups();
+			try {
+				HttpPost request = new HttpPost(uriBuilder.build());
+				JSONObject body = new JSONObject()
+						.put(J_WS_REST_GET_MEMBERS_REQUEST, new JSONObject()
+								.put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject()
+										.put(J_UUID, uuid) })
+								.put(J_INCLUDE_SUBJECT_DETAIL, true));
+				return executeGetMembers(request, body, handler);
+			} catch (RuntimeException | URISyntaxException e) {
+				throw processException(e, uriBuilder, "Get group by UUID (with members)");
+			}
+		}
+	}
+
+	private void getAllGroups(final ResultsHandler handler, final OperationOptions options) {
+		boolean getMembers = isGetMembers(options);
+		if (!getMembers) {
+			getAllGroupsNoMembers(handler);
+		} else {
+			ResultsHandler localHandler = connectorObject -> getGroupByUuid(connectorObject.getUid().getUidValue(), handler, options);
+			getAllGroupsNoMembers(localHandler);
+		}
+	}
+
+	private void getAllGroupsNoMembers(ResultsHandler handler) {
+		URIBuilder uriBuilder = getUriBuilderForGroups();
+		try {
+			HttpPost request = new HttpPost(uriBuilder.build());
+			String configuredExportStem = configuration.getExportStem();
+			JSONObject body = new JSONObject()
+					.put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject()
+							.put(J_WS_QUERY_FILTER, new JSONObject()
+									.put(J_QUERY_FILTER_TYPE, VAL_FIND_BY_STEM_NAME)
+									.put(J_STEM_NAME, configuredExportStem != null ? configuredExportStem : DEFAULT_EXPORT_STEM)
+									.put(J_STEM_NAME_SCOPE, VAL_ALL_IN_SUBTREE)));
+			executeFindGroups(request, body, handler);
+		} catch (RuntimeException | URISyntaxException e) {
+			throw processException(e, uriBuilder, "Get all groups");
+		}
+	}
+
+	private boolean executeFindGroups(HttpPost request, JSONObject body, ResultsHandler handler) {
+		JSONObject response = callRequest(request, body);
+		checkSuccess(response, J_WS_FIND_GROUPS_RESULTS);
+		JSONArray groups = getArray(response, false, J_WS_FIND_GROUPS_RESULTS, J_GROUP_RESULTS);
+		if (groups != null) {
+			for (Object group : groups) {
+				if (!handleGroupJsonObject(group, handler)) {
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+
+	private boolean executeGetMembers(HttpPost request, JSONObject body, ResultsHandler handler) {
+		JSONObject response = callRequest(request, body);
+		checkSuccess(response, J_WS_GET_MEMBERS_RESULTS);
+		JSONObject gObject = (JSONObject) get(response, J_WS_GET_MEMBERS_RESULTS, J_RESULTS, J_WS_GROUP);
+		String name = getStringOrNull(gObject, J_NAME);
+		if (groupNameMatches(name)) {
+			ConnectorObjectBuilder builder = startGroupObjectBuilding(gObject, name);
+			List<String> members = new ArrayList<>();
+			JSONArray membersJsonArray = getArray(response, false, J_WS_GET_MEMBERS_RESULTS, J_RESULTS, J_WS_SUBJECTS);
+			if (membersJsonArray != null) {
+				for (Object memberObject : membersJsonArray) {
+					JSONObject member = (JSONObject) memberObject;
+					String sourceId = getStringOrNull(member, J_SOURCE_ID);
+					if (sourceId == null || !sourceId.equals(configuration.getSubjectSource())) {
+						LOG.info("Skipping member with wrong source (e.g. one that is not a person) (source={0})", sourceId);
+					} else {
+						String subjectId = getStringOrNull(member, J_ID);
+						if (subjectId != null) {
+							members.add(subjectId);
+						} else {
+							LOG.info("Skipping unnamed member (source={0})", member);
+						}
+					}
+				}
+				builder.addAttribute(ATTR_MEMBER, members);
+			}
+			return handler.handle(builder.build());
+		} else {
+			return true;
+		}
+	}
+
+	private boolean handleGroupJsonObject(Object group, ResultsHandler handler) {
+		if (group instanceof JSONObject) {
+			JSONObject gObject = (JSONObject) group;
+			String name = getStringOrNull(gObject, J_NAME);
+			if (groupNameMatches(name)) {
+				return handler.handle(startGroupObjectBuilding(gObject, name).build());
+			} else {
+				return true;
+			}
+		} else {
+			throw new IllegalStateException("Expected group as JSONObject, got " + group);
+		}
+	}
+
+	private ConnectorObjectBuilder startGroupObjectBuilding(JSONObject gObject, String name) {
+		String extension = getStringOrNull(gObject, J_EXTENSION);
+		String uuid = getStringOrNull(gObject, J_UUID);
+		ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
+		builder.setObjectClass(getObjectClass());
+		builder.setUid(uuid);
+		builder.setName(name);
+		builder.addAttribute(ATTR_EXTENSION, extension);
+		return builder;
+	}
+
+	void test() {
+		if (configuration.getTestStem() != null) {
+			checkStemExists(configuration.getTestStem());
+		}
+		if (configuration.getTestGroup() != null) {
+			checkGroupExists(configuration.getTestGroup());
+		}
+	}
+
+	private void checkStemExists(String stemName) {
+		URIBuilder uriBuilder = getUriBuilderForStems();
+		JSONArray stems;
+		try {
+			HttpPost request = new HttpPost(uriBuilder.build());
+			JSONObject body = new JSONObject()
+					.put(J_WS_REST_FIND_STEMS_REQUEST, new JSONObject()
+							.put(J_WS_STEM_QUERY_FILTER, new JSONObject()
+									.put(J_STEM_QUERY_FILTER_TYPE, VAL_FIND_BY_STEM_NAME)
+									.put(J_STEM_NAME, stemName)));
+			JSONObject response = callRequest(request, body);
+			checkSuccess(response, J_WS_FIND_STEMS_RESULTS);
+			stems = getArray(response, true, J_WS_FIND_STEMS_RESULTS, J_STEM_RESULTS);
+		} catch (RuntimeException | URISyntaxException e) {
+			throw processException(e, uriBuilder, "Find stems request");
+		}
+		if (stems.length() == 0) {
+			throw new ConnectorException("Expected to find the stem '" + stemName + "', found none");
+		}
+	}
+
+	private void checkGroupExists(String groupName) {
+		List<ConnectorObject> groups = new ArrayList<>();
+		getGroupByName(groupName, groups::add, null);
+		LOG.info("getGroupByName found {0} group(s): {1}", groups.size(), groups);
+		if (groups.isEmpty()) {
+			throw new ConnectorException("Expected to find the group '" + groupName + "', found none");
+		}
+	}
+
+	ObjectClassInfoBuilder buildSchema() {
+		ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder();
+		builder.setType(OBJECT_CLASS_NAME);
+		builder.addAttributeInfo(
+				new AttributeInfoBuilder(Name.NAME, String.class)
+						.setNativeName(ATTR_NAME)
+						.setRequired(true)
+						.build());
+		builder.addAttributeInfo(
+				new AttributeInfoBuilder(Uid.NAME, String.class)
+						.setNativeName(ATTR_UUID)
+						.setRequired(true)
+						.build());
+		builder.addAttributeInfo(
+				new AttributeInfoBuilder(ATTR_EXTENSION, String.class)
+						.build());
+		builder.addAttributeInfo(
+				new AttributeInfoBuilder(ATTR_MEMBER, String.class)
+						.setMultiValued(true)
+						.setReturnedByDefault(false)
+						.build());
+		return builder;
+	}
+
+	private boolean isGetMembers(OperationOptions options) {
+		String[] attrs = options != null ? options.getAttributesToGet() : null;
+		return attrs != null && Arrays.asList(attrs).contains(ATTR_MEMBER);
+	}
+}
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java
index 8c4dbdc..3563f1f 100644
--- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java
+++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java
@@ -1,5 +1,5 @@
-/**
- * Copyright (c) 2016 Evolveum
+/*
+ * Copyright (c) 2019 Evolveum
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -32,196 +32,217 @@
  */
 @SuppressWarnings("WeakerAccess")
 public class GrouperConfiguration extends AbstractConfiguration implements StatefulConfiguration {
-	
-	private static final Log LOG = Log.getLog(GrouperConfiguration.class);
-
-	private static final String DEFAULT_GROUP_SOURCE_ID = "g:gsa";
-
-	private String baseUrl;
-	private String username;
-	private GuardedString password;
-	private String superGroup;
-	private String[] groupIncludePattern;
-	private String[] groupExcludePattern;
-	private Boolean ignoreSslValidation;
-	private String subjectSource;
-	private String groupSource;
-	private String exportStem;
-
-	// getter and setter methods for "baseUrl" attribute:
-	@ConfigurationProperty(order = 1, displayMessageKey = "baseUrl.display", helpMessageKey = "baseUrl.help", required = true)
-	public String getBaseUrl() {
-		return baseUrl;
-	}
-
-	public void setBaseUrl(String baseUrl) {
-		this.baseUrl = baseUrl;
-	}
-
-	// getter and setter methods for "username" attribute:
-	@ConfigurationProperty(order = 2, displayMessageKey = "username.display", helpMessageKey = "username.help", required = true)
-	public String getUsername() {
-		return username;
-	}
-
-	public void setUsername(String name) {
-		this.username = name;
-	}
-
-	private String stringPassword = "";
-
-	@ConfigurationProperty(order = 3, displayMessageKey = "password.display", helpMessageKey = "password.help", required = true, confidential = false)
-	public GuardedString getPassword() {
-		return password;
-	}
-
-	public void setPassword(GuardedString password) {
-		this.password = password;
-	}
-	
-	public String getStringPassword() {
-		password.access(new GuardedString.Accessor() {
-			@Override
-			public void access(char[] clearChars) {
-				stringPassword = new String(clearChars);
-			}
-		});
-		return stringPassword;
-	}
-
-	/**
-	 * "Super group" that marks groups and users that are to be visible through this connector.
-	 *
-	 * Used for Account and Group object classes; ignored for PlainGroup object class.
-	 */
-	@ConfigurationProperty(order = 4, displayMessageKey = "superGroup.display", helpMessageKey = "superGroup.help", required = true)
-	public String getSuperGroup() {
-		return superGroup;
-	}
-
-	public void setSuperGroup(String superGroup) {
-		this.superGroup = superGroup;
-	}
-
-	/**
-	 * Used to limit group membership to a subset of all groups. Applicable to Account object class.
-	 */
-	@ConfigurationProperty(order = 5, displayMessageKey = "groupIncludePattern.display", helpMessageKey = "groupIncludePattern.help", required = true)
-	public String[] getGroupIncludePattern() {
-		return groupIncludePattern;
-	}
-
-	public void setGroupIncludePattern(String[] groupIncludePattern) {
-		this.groupIncludePattern = groupIncludePattern;
-	}
-
-	/**
-	 * Used to limit group membership to a subset of all groups. Applicable to Account object class.
-	 */
-	@ConfigurationProperty(order = 6, displayMessageKey = "groupExcludePattern.display", helpMessageKey = "groupExcludePattern.help", required = true)
-	public String[] getGroupExcludePattern() {
-		return groupExcludePattern;
-	}
-
-	@SuppressWarnings("unused")
-	public void setGroupExcludePattern(String[] groupExcludePattern) {
-		this.groupExcludePattern = groupExcludePattern;
-	}
-
-	@ConfigurationProperty(order = 7, displayMessageKey = "ignoreSslValidation.display", helpMessageKey = "ignoreSslValidation.help")
-	public Boolean getIgnoreSslValidation() {
-		return ignoreSslValidation;
-	}
-
-	public void setIgnoreSslValidation(Boolean ignoreSslValidation) {
-		this.ignoreSslValidation = ignoreSslValidation;
-	}
-
-	/**
-	 * Used to limit subjects returned by this connector. Applicable to Account and PlainGroup object class.
-	 */
-	@ConfigurationProperty(order = 8, displayMessageKey = "subjectSource.display", helpMessageKey = "subjectSource.help", required = true)
-	public String getSubjectSource() {
-		return subjectSource;
-	}
-
-	public void setSubjectSource(String subjectSource) {
-		this.subjectSource = subjectSource;
-	}
-
-	/**
-	 * Used to limit groups returned by this connector. Applicable to Group object class. Usually not needed to change.
-	 */
-	@ConfigurationProperty(order = 9, displayMessageKey = "groupSource.display", helpMessageKey = "groupSource.help")
-	public String getGroupSource() {
-		return groupSource != null ? groupSource : DEFAULT_GROUP_SOURCE_ID;
-	}
-
-	@SuppressWarnings("unused")
-	public void setGroupSource(String groupSource) {
-		this.groupSource = groupSource;
-	}
-
-	/**
-	 * Used to specify root stem for groups returned by this connector. Applicable to PlainGroup object class.
-	 */
-	@ConfigurationProperty(order = 10, displayMessageKey = "exportStem.display", helpMessageKey = "exportStem.help")
-	public String getExportStem() {
-		return exportStem;
-	}
-
-	public void setExportStem(String exportStem) {
-		this.exportStem = exportStem;
-	}
-
-	@Override
-	public void validate() {
-		String exceptionMsg;
-		if (baseUrl == null || StringUtil.isBlank(baseUrl)) {
-			exceptionMsg = "Base url is not provided.";
-		} else if (username == null || StringUtil.isBlank(username)) {
-			exceptionMsg = "Name is not provided.";
-		} else if (password == null) {
-			exceptionMsg = "Password is not provided.";
-		} else if (superGroup == null) {
-			exceptionMsg = "Super group is not provided.";
-		} else if (groupIncludePattern == null || groupIncludePattern.length == 0) {
-			exceptionMsg = "Group include pattern is not provided.";
-		} else if (subjectSource == null) {
-			exceptionMsg = "Subject source is not provided.";
-		} else {
-			return;
-		}
-		LOG.error("{0}", exceptionMsg);
-		throw new ConfigurationException(exceptionMsg);
-	}
-	
-	@Override
-	public void release() {
-		LOG.info("The release of configuration resources is being performed");
-		this.baseUrl = null;
-		this.password = null;
-		this.username = null;
-		this.superGroup = null;
-		this.groupIncludePattern = null;
-		this.groupExcludePattern = null;
-		this.subjectSource = null;
-		this.groupSource = null;
-		this.exportStem = null;
-	}
-
-	@Override
-	public String toString() {
-		return "GrouperConfiguration{" +
-				"baseUrl='" + baseUrl + '\'' +
-				", username='" + username + '\'' +
-				", superGroup='" + superGroup + '\'' +
-				", groupIncludePattern=" + Arrays.toString(groupIncludePattern) +
-				", groupExcludePattern=" + Arrays.toString(groupExcludePattern) +
-				", ignoreSslValidation=" + ignoreSslValidation +
-				", subjectSource='" + subjectSource + '\'' +
-				", groupSource='" + groupSource + '\'' +
-				", exportStem='" + exportStem + '\'' +
-				'}';
-	}
+
+    private static final Log LOG = Log.getLog(GrouperConfiguration.class);
+
+    private static final String DEFAULT_GROUP_SOURCE_ID = "g:gsa";
+
+    private String baseUrl;
+    private String username;
+    private GuardedString password;
+    private Boolean ignoreSslValidation;
+
+    private String exportStem;
+    private String[] groupIncludePattern;
+    private String[] groupExcludePattern;
+    private String subjectSource;
+    private String testStem;
+    private String testGroup;
+
+    // deprecated (to be removed)
+    private String superGroup;
+    private String groupSource;
+
+    @ConfigurationProperty(order = 10, displayMessageKey = "baseUrl.display", helpMessageKey = "baseUrl.help", required = true)
+    public String getBaseUrl() {
+        return baseUrl;
+    }
+
+    public void setBaseUrl(String baseUrl) {
+        this.baseUrl = baseUrl;
+    }
+
+    @ConfigurationProperty(order = 20, displayMessageKey = "username.display", helpMessageKey = "username.help", required = true)
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String name) {
+        this.username = name;
+    }
+
+    @ConfigurationProperty(order = 30, displayMessageKey = "password.display", helpMessageKey = "password.help", required = true, confidential = true)
+    public GuardedString getPassword() {
+        return password;
+    }
+
+    public void setPassword(GuardedString password) {
+        this.password = password;
+    }
+
+    public String getPasswordPlain() {
+        StringBuilder plain = new StringBuilder();
+        password.access(clearChars -> plain.append(new String(clearChars)));
+        return plain.toString();
+    }
+
+    /**
+     * Should we ignore SSL validation issues when connecting to the Grouper REST service? Do not use in production.
+     */
+    @ConfigurationProperty(order = 40, displayMessageKey = "ignoreSslValidation.display", helpMessageKey = "ignoreSslValidation.help")
+    public Boolean getIgnoreSslValidation() {
+        return ignoreSslValidation;
+    }
+
+    public void setIgnoreSslValidation(Boolean ignoreSslValidation) {
+        this.ignoreSslValidation = ignoreSslValidation;
+    }
+
+    /**
+     * Used to specify root stem for groups returned by this connector. The default is ":" (the whole tree).
+     */
+    @ConfigurationProperty(order = 50, displayMessageKey = "exportStem.display", helpMessageKey = "exportStem.help")
+    public String getExportStem() {
+        return exportStem;
+    }
+
+    public void setExportStem(String exportStem) {
+        this.exportStem = exportStem;
+    }
+
+    /**
+     * Which groups should be visible to this connector?
+     */
+    @ConfigurationProperty(order = 60, displayMessageKey = "groupIncludePattern.display", helpMessageKey = "groupIncludePattern.help")
+    public String[] getGroupIncludePattern() {
+        return groupIncludePattern;
+    }
+
+    public void setGroupIncludePattern(String[] groupIncludePattern) {
+        this.groupIncludePattern = groupIncludePattern;
+    }
+
+    /**
+     * Which groups should be hidden (invisible) to this connector?
+     */
+    @ConfigurationProperty(order = 70, displayMessageKey = "groupExcludePattern.display", helpMessageKey = "groupExcludePattern.help")
+    public String[] getGroupExcludePattern() {
+        return groupExcludePattern;
+    }
+
+    public void setGroupExcludePattern(String[] groupExcludePattern) {
+        this.groupExcludePattern = groupExcludePattern;
+    }
+
+    /**
+     * Used to limit subjects returned by this connector.
+     */
+    @ConfigurationProperty(order = 80, displayMessageKey = "subjectSource.display", helpMessageKey = "subjectSource.help", required = true)
+    public String getSubjectSource() {
+        return subjectSource;
+    }
+
+    public void setSubjectSource(String subjectSource) {
+        this.subjectSource = subjectSource;
+    }
+
+    /**
+     * Used to specify stem that is fetched during Test Connection (if any).
+     */
+    @ConfigurationProperty(order = 90, displayMessageKey = "testStem.display", helpMessageKey = "testStem.help")
+    public String getTestStem() {
+        return testStem;
+    }
+
+    public void setTestStem(String testStem) {
+        this.testStem = testStem;
+    }
+
+    /**
+     * Used to specify group that is fetched during Test Connection (if any).
+     */
+    @ConfigurationProperty(order = 100, displayMessageKey = "testGroup.display", helpMessageKey = "testGroup.help")
+    public String getTestGroup() {
+        return testGroup;
+    }
+
+    public void setTestGroup(String testGroup) {
+        this.testGroup = testGroup;
+    }
+
+    /**
+     * Currently unused. It is kept here just to avoid breaking older configurations. Will be removed eventually.
+     */
+    @Deprecated
+    @ConfigurationProperty(order = 9900, displayMessageKey = "superGroup.display", helpMessageKey = "superGroup.help")
+    public String getSuperGroup() {
+        return superGroup;
+    }
+
+    @Deprecated
+    public void setSuperGroup(String superGroup) {
+        this.superGroup = superGroup;
+    }
+
+    /**
+     * Currently unused. It is kept here just to avoid breaking older configurations. Will be removed eventually.
+     */
+    @Deprecated
+    @ConfigurationProperty(order = 9901, displayMessageKey = "groupSource.display", helpMessageKey = "groupSource.help")
+    public String getGroupSource() {
+        return groupSource != null ? groupSource : DEFAULT_GROUP_SOURCE_ID;
+    }
+
+    @Deprecated
+    public void setGroupSource(String groupSource) {
+        this.groupSource = groupSource;
+    }
+
+
+    @Override
+    public void validate() {
+        String exceptionMsg;
+        if (StringUtil.isBlank(baseUrl)) {
+            exceptionMsg = "Base URL is not provided.";
+        } else if (StringUtil.isBlank(username)) {
+            exceptionMsg = "Name is not provided.";
+        } else if (password == null) {
+            exceptionMsg = "Password is not provided.";
+        } else if (subjectSource == null) {
+            exceptionMsg = "Subject source is not provided.";
+        } else {
+            return;
+        }
+        LOG.error("{0}", exceptionMsg);
+        throw new ConfigurationException(exceptionMsg);
+    }
+
+    @Override
+    public void release() {
+        this.baseUrl = null;
+        this.username = null;
+        this.password = null;
+        this.ignoreSslValidation = null;
+        this.exportStem = null;
+        this.groupIncludePattern = null;
+        this.groupExcludePattern = null;
+        this.subjectSource = null;
+        this.testGroup = null;
+        this.superGroup = null;
+        this.groupSource = null;
+    }
+
+    @Override
+    public String toString() {
+        return "GrouperConfiguration{" +
+                "baseUrl='" + baseUrl + '\'' +
+                ", username='" + username + '\'' +
+                ", ignoreSslValidation=" + ignoreSslValidation +
+                ", exportStem='" + exportStem + '\'' +
+                ", groupIncludePattern=" + Arrays.toString(groupIncludePattern) +
+                ", groupExcludePattern=" + Arrays.toString(groupExcludePattern) +
+                ", subjectSource='" + subjectSource + '\'' +
+                ", testGroup='" + testGroup + '\'' +
+                '}';
+    }
 }
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java
index f770ebc..1280350 100644
--- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java
+++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java
@@ -1,5 +1,5 @@
-/**
- * Copyright (c) 2017 Evolveum
+/*
+ * Copyright (c) 2019 Evolveum
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,121 +20,97 @@
 import org.identityconnectors.framework.common.exceptions.ConfigurationException;
 import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException;
 import org.identityconnectors.framework.common.objects.*;
-import org.identityconnectors.framework.common.objects.filter.*;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+import org.identityconnectors.framework.common.objects.filter.FilterTranslator;
 import org.identityconnectors.framework.spi.Configuration;
 import org.identityconnectors.framework.spi.Connector;
 import org.identityconnectors.framework.spi.ConnectorClass;
-import org.identityconnectors.framework.spi.operations.*;
-
-import java.util.List;
+import org.identityconnectors.framework.spi.operations.SchemaOp;
+import org.identityconnectors.framework.spi.operations.SearchOp;
+import org.identityconnectors.framework.spi.operations.TestOp;
 
 /**
- * @author surmanek
- * @author mederly
- *
+ * Configuration for the Grouper connector.
  */
 @ConnectorClass(displayNameKey = "GrouperConnector.rest.display", configurationClass = GrouperConfiguration.class)
 public class GrouperConnector implements TestOp, SchemaOp, Connector, SearchOp<Filter> {
 
-	private static final Log LOG = Log.getLog(GrouperConnector.class);
-	private GrouperConfiguration configuration;
-	private Processor processor;
-	private AccountProcessor accountProcessor;
-	private StandardGroupProcessor standardGroupProcessor;
-	private PlainGroupProcessor plainGroupProcessor;
-
-	private static final String PROJECT_NAME = "PROJECT";
-	private static final String ATTR_GROUPS = "groups";
-
-	@Override
-	public GrouperConfiguration getConfiguration() {
-		return configuration;
-	}
-
-	@Override
-	public void init(Configuration configuration) {
-		if (configuration == null) {
-			LOG.error("Initialization of the configuration failed: Configuration is not provided.");
-			throw new ConfigurationException(
-					"Initialization of the configuration failed: Configuration is not provided.");
-		}
-		this.configuration = (GrouperConfiguration) configuration;
-		this.configuration.validate();
-		this.processor = new Processor(this.configuration);
-		this.accountProcessor = new AccountProcessor(processor);
-		this.standardGroupProcessor = new StandardGroupProcessor(processor);
-		this.plainGroupProcessor = new PlainGroupProcessor(processor);
-	}
-
-	@Override
-	public void dispose() {
-		configuration = null;
-		processor = null;
-		accountProcessor = null;
-		standardGroupProcessor = null;
-		plainGroupProcessor = null;
-	}
-
-	@Override
-	public void test() {
-		LOG.info("Testing connection...");
-		standardGroupProcessor.test();
-		LOG.ok("Testing finished successfully.");
-	}
-
-	@Override
-	public Schema schema() {
-		SchemaBuilder schemaBuilder = new SchemaBuilder(GrouperConnector.class);
-
-		schemaBuilder.defineObjectClass(accountProcessor.buildSchema().build());
-		schemaBuilder.defineObjectClass(standardGroupProcessor.buildSchema().build());
-		schemaBuilder.defineObjectClass(plainGroupProcessor.buildSchema().build());
-
-		return schemaBuilder.build();
-	}
-
-	@Override
-	public FilterTranslator<Filter> createFilterTranslator(ObjectClass arg0, OperationOptions arg1) {
-		return new FilterTranslator<Filter>() {
-			@Override
-			public List<Filter> translate(Filter filter) {
-				return CollectionUtil.newList(filter);
-			}
-		};
-	}
-
-	@Override
-	public void executeQuery(ObjectClass objClass, Filter filter, ResultsHandler handler, OperationOptions options) {
-		LOG.info("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Execute Query-Parameters~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
-		if (objClass == null) {
-			LOG.error("Get operation failed: Attribute Object Class is not provided.");
-			throw new InvalidAttributeValueException("Attribute Object Class is not provided.");
-		} else
-			LOG.info("ObjectClass: {0}", objClass.toString());
-
-		if (handler == null) {
-			LOG.error("Get operation failed: Attribute Result Handler is not provided.");
-			throw new InvalidAttributeValueException("Attribute Result Handler is not provided.");
-		} else
-			LOG.info("Execute Query-Handler: {0}", handler.toString());
-
-		if (options == null) {
-			LOG.error("Get operation failed: Attribute Options is not provided.");
-			throw new InvalidAttributeValueException("Attribute Options is not provided.");
-		} else
-			LOG.info("Options: {0}", options.toString());
-
-		LOG.info("Filter: {0}", filter);
-		LOG.info("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
-
-		if (objClass.is(ObjectClass.ACCOUNT_NAME)) {
-			accountProcessor.read(filter, handler, options);
-		} else if (objClass.is(ObjectClass.GROUP_NAME)) {
-			standardGroupProcessor.read(filter, handler, options);
-		} else if (objClass.is(plainGroupProcessor.getObjectClass().getObjectClassValue())) {
-			plainGroupProcessor.read(filter, handler, options);
-		}
-	}
-
-
+    private static final Log LOG = Log.getLog(GrouperConnector.class);
+
+    private GrouperConfiguration configuration;
+    private GroupProcessor groupProcessor;
+
+    @Override
+    public GrouperConfiguration getConfiguration() {
+        return configuration;
+    }
+
+    @Override
+    public void init(Configuration configuration) {
+        if (configuration == null) {
+            LOG.error("Initialization of the configuration failed: Configuration is not provided.");
+            throw new ConfigurationException(
+                    "Initialization of the configuration failed: Configuration is not provided.");
+        }
+        this.configuration = (GrouperConfiguration) configuration;
+        this.configuration.validate();
+        this.groupProcessor = new GroupProcessor(this.configuration);
+    }
+
+    @Override
+    public void dispose() {
+        configuration = null;
+        groupProcessor = null;
+    }
+
+    @Override
+    public void test() {
+        LOG.info("Testing connection...");
+        groupProcessor.test();
+        LOG.ok("Testing finished successfully.");
+    }
+
+    @Override
+    public Schema schema() {
+        SchemaBuilder schemaBuilder = new SchemaBuilder(GrouperConnector.class);
+        schemaBuilder.defineObjectClass(groupProcessor.buildSchema().build());
+        return schemaBuilder.build();
+    }
+
+    @Override
+    public FilterTranslator<Filter> createFilterTranslator(ObjectClass arg0, OperationOptions arg1) {
+        return CollectionUtil::newList;
+    }
+
+    @Override
+    public void executeQuery(ObjectClass objClass, Filter filter, ResultsHandler handler, OperationOptions options) {
+        LOG.info("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Execute Query-Parameters~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
+        if (objClass == null) {
+            LOG.error("Get operation failed: object class is not provided.");
+            throw new InvalidAttributeValueException("Object class is not provided.");
+        } else if (!objClass.is(groupProcessor.getObjectClass().getObjectClassValue())) {
+            throw new IllegalArgumentException("Unsupported object class: " + objClass);
+        } else {
+            LOG.info("ObjectClass: {0}", objClass);
+        }
+
+        if (handler == null) {
+            LOG.error("Get operation failed: result handler is not provided.");
+            throw new InvalidAttributeValueException("Result handler is not provided.");
+        } else {
+            LOG.info("Execute Query-Handler: {0}", handler);
+        }
+
+        if (options == null) {
+            LOG.error("Get operation failed: options are not provided.");
+            throw new InvalidAttributeValueException("Options are not provided.");
+        } else {
+            LOG.info("Options: {0}", options);
+        }
+
+        LOG.info("Filter: {0}", filter);
+        LOG.info("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
+
+        groupProcessor.read(filter, handler, options);
+    }
 }
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/PlainGroupProcessor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/PlainGroupProcessor.java
deleted file mode 100644
index b9ebbf5..0000000
--- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/PlainGroupProcessor.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*******************************************************************************
- * Copyright 2017 Evolveum
- * 
- * 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.
- ******************************************************************************/
-package com.evolveum.polygon.connector.grouper.rest;
-
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URIBuilder;
-import org.identityconnectors.framework.common.objects.*;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static com.evolveum.polygon.connector.grouper.rest.Processor.*;
-
-/**
- * @author surmanek
- * @author mederly
- *
- */
-public class PlainGroupProcessor extends AbstractGroupProcessor {
-
-	public static final String OBJECT_CLASS_NAME = "PlainGroup";
-	public static final String ATTR_MEMBER = "member";
-
-	PlainGroupProcessor(Processor processor) {
-		super(processor);
-	}
-
-	ObjectClassInfoBuilder buildSchema() {
-		ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder();
-
-		builder.setType(OBJECT_CLASS_NAME);
-		AttributeInfoBuilder extension = new AttributeInfoBuilder(ATTR_EXTENSION, String.class);
-		builder.addAttributeInfo(extension.build());
-
-		AttributeInfoBuilder member = new AttributeInfoBuilder(ATTR_MEMBER, String.class);
-		member.setMultiValued(true);
-		member.setReturnedByDefault(false);
-		builder.addAttributeInfo(member.build());
-
-		return builder;
-	}
-
-	protected void getAllGroups(final ResultsHandler handler, final OperationOptions options) {
-		boolean isGetMembers = isGetMembers(options);
-		if (!isGetMembers) {
-			getAllGroupsNoMembers(handler);
-		} else {
-			ResultsHandler localHandler = new ResultsHandler() {
-				@Override
-				public boolean handle(ConnectorObject connectorObject) {
-					return getGroupByUuid(connectorObject.getUid().getUidValue(), handler, options);
-				}
-			};
-			getAllGroupsNoMembers(localHandler);
-		}
-	}
-
-	private void getAllGroupsNoMembers(ResultsHandler handler) {
-		URIBuilder uriBuilder = processor.getURIBuilder()
-				.setPath(URI_BASE_PATH + PATH_GROUPS);
-		try {
-			HttpPost request = new HttpPost(uriBuilder.build());
-			JSONObject body = new JSONObject()
-					.put("WsRestFindGroupsRequest", new JSONObject()
-							.put("wsQueryFilter", new JSONObject()
-									.put("queryFilterType", "FIND_BY_STEM_NAME")
-									.put("stemName", getConfiguration().getExportStem())
-									.put("stemNameScope", "ALL_IN_SUBTREE")));
-			executeFindGroupsResponse(request, body, handler);
-		} catch (RuntimeException | URISyntaxException e) {
-			throw processor.processException(e, uriBuilder, "Get all groups");
-		}
-	}
-
-	@Override
-	boolean getGroupByUuid(String uuid, ResultsHandler handler, OperationOptions options) {
-		if (!isGetMembers(options)) {
-			return getGroupByUuid(uuid, handler);
-		} else {
-			URIBuilder uriBuilder = processor.getURIBuilder()
-					.setPath(URI_BASE_PATH + PATH_GROUPS);
-			try {
-				HttpPost request = new HttpPost(uriBuilder.build());
-				JSONObject body = new JSONObject()
-						.put("WsRestGetMembersRequest", new JSONObject()
-								.put("wsGroupLookups", new JSONObject[] { new JSONObject()
-										.put("uuid", uuid) })
-								.put("includeSubjectDetail", true));
-				return executeGetGroupWithMembersResponse(request, body, handler);
-			} catch (RuntimeException | URISyntaxException e) {
-				throw processor.processException(e, uriBuilder, "Get all groups");
-			}
-
-		}
-	}
-
-	@Override
-	void getGroupByName(String name, ResultsHandler handler, OperationOptions options) {
-		if (!isGetMembers(options)) {
-			getGroupByName(name, handler);
-		} else {
-			URIBuilder uriBuilder = processor.getURIBuilder()
-					.setPath(URI_BASE_PATH + PATH_GROUPS);
-			try {
-				HttpPost request = new HttpPost(uriBuilder.build());
-				JSONObject body = new JSONObject()
-						.put("WsRestGetMembersRequest", new JSONObject()
-								.put("wsGroupLookups", new JSONObject[] { new JSONObject()
-										.put("groupName", name) })
-								.put("includeSubjectDetail", true));
-				executeGetGroupWithMembersResponse(request, body, handler);
-			} catch (RuntimeException | URISyntaxException e) {
-				throw processor.processException(e, uriBuilder, "Get all groups");
-			}
-		}
-	}
-
-	private boolean executeGetGroupWithMembersResponse(HttpPost request, JSONObject body, ResultsHandler handler) {
-		System.out.println("Request = " + body.toString());
-		JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON);
-		System.out.println("Got response: " + response);
-		processor.checkSuccess(response, WS_GET_MEMBERS_RESULTS);
-
-		JSONObject gObject = (JSONObject) processor.get(response, WS_GET_MEMBERS_RESULTS, RESULTS, WS_GROUP);
-		String name = processor.getStringOrNull(gObject, "name");
-		if (processor.groupNameMatches(name)) {
-			String extension = processor.getStringOrNull(gObject, "extension");
-			String uuid = processor.getStringOrNull(gObject, "uuid");
-			ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
-			builder.setObjectClass(getObjectClass());
-			builder.setUid(uuid);
-			builder.setName(name);
-			builder.addAttribute(ATTR_EXTENSION, extension);
-
-			List<String> subjects = new ArrayList<>();
-			JSONArray members = processor.getArray(response, false, WS_GET_MEMBERS_RESULTS, RESULTS, WS_SUBJECTS);
-			if (members != null) {
-				for (Object memberObject : members) {
-					JSONObject member = (JSONObject) memberObject;
-					String sourceId = processor.getStringOrNull(member, "sourceId");
-					if (sourceId == null || !sourceId.equals(getConfiguration().getSubjectSource())) {
-						LOG.info("Skipping non-person member (source={0})", sourceId);
-						continue;
-					}
-					String subjectId = processor.getStringOrNull(member, "id");
-					if (subjectId != null) {
-						subjects.add(subjectId);
-					} else {
-						LOG.info("Skipping unnamed member (source={0})", member);
-					}
-				}
-				builder.addAttribute(ATTR_MEMBER, subjects);
-			}
-			return handler.handle(builder.build());
-		} else {
-			return true;
-		}
-	}
-
-	@Override
-	protected ObjectClass getObjectClass() {
-		return new ObjectClass(OBJECT_CLASS_NAME);
-	}
-
-	private boolean isGetMembers(OperationOptions options) {
-		String[] attrs = options.getAttributesToGet();
-		return attrs != null && Arrays.asList(attrs).contains(ATTR_MEMBER);
-	}
-}
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java
index 9fa18b4..dd965e1 100644
--- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java
+++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java
@@ -1,20 +1,21 @@
-/*******************************************************************************
- * Copyright 2017 Evolveum
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); 
- * you may not use this file except in compliance with the License.  
+/*
+ * Copyright (c) 2019 Evolveum
+ *
+ * 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
- * 
+ *
+ *     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.
- ******************************************************************************/
+ * 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.
+ */
 package com.evolveum.polygon.connector.grouper.rest;
 
 import org.apache.commons.codec.binary.Base64;
-import org.apache.http.HttpEntity;
 import org.apache.http.client.methods.*;
 import org.apache.http.client.utils.URIBuilder;
 import org.apache.http.conn.ssl.NoopHostnameVerifier;
@@ -27,180 +28,128 @@
 import org.apache.http.util.EntityUtils;
 import org.identityconnectors.common.logging.Log;
 import org.identityconnectors.framework.common.exceptions.*;
-import org.identityconnectors.framework.common.objects.*;
-import org.identityconnectors.framework.common.objects.filter.*;
 import org.json.JSONArray;
 import org.json.JSONObject;
 
 import java.io.*;
 import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
 import java.security.KeyManagementException;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
 import java.util.regex.Pattern;
 
 /**
- * @author surmanek
- *
+ * Contains generic logic for handling REST operations over Grouper.
  */
 public class Processor {
 
 	static final Log LOG = Log.getLog(GrouperConnector.class);
-	static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8";
-	public static final String WS_FIND_STEMS_RESULTS = "WsFindStemsResults";
-	public static final String RESULT_METADATA = "resultMetadata";
-	public static final String SUCCESS = "success";
-	public static final String STEM_RESULTS = "stemResults";
-	public static final String PATH_STEMS = "/stems";
-	public static final String WS_GET_MEMBERS_RESULTS = "WsGetMembersResults";
-	public static final String RESULTS = "results";
-	public static final String WS_SUBJECTS = "wsSubjects";
-	public static final String WS_REST_GET_MEMBERS_REQUEST = "WsRestGetMembersRequest";
-	public static final String WS_GROUP_LOOKUPS = "wsGroupLookups";
-	public static final String GROUP_NAME = "groupName";
-	public static final String WS_GET_GROUPS_RESULTS = "WsGetGroupsResults";
-	public static final String WS_GROUPS = "wsGroups";
-	public static final String WS_GROUP = "wsGroup";
-	GrouperConfiguration configuration;
-
-	public static final String URI_BASE_PATH = "/grouper-ws/servicesRest/json/v2_4_000";
-	public static final String PATH_GROUPS = "/groups";
-	public static final String PATH_SUBJECTS = "/subjects";
-
-	public Processor(GrouperConfiguration configuration) {
-		this.configuration = configuration;
-	}
 
-	//put objects of array1 to the end of array2
-	private JSONArray concatJSONArrays(JSONArray array1, JSONArray array2){
-		for (Object obj : array1){
-			array2.put(obj);
-		}
-		return array2;
-	}
-
-	JSONObject callRequest(HttpEntityEnclosingRequestBase request, JSONObject jo, Boolean parseResult,
-			String contentType) {
-		// don't log request here - password field !!!
-		if (contentType != null)
-			request.addHeader("Content-Type", contentType);
-		request.addHeader("Authorization", "Basic " + authEncoding());
-		HttpEntity entity;
-		try {
-			entity = new ByteArrayEntity(jo.toString().getBytes("UTF-8"));
-		} catch (UnsupportedEncodingException e1) {
-			String exceptionMsg = "Creating request entity failed: problem occurred during entity encoding.";
-			LOG.error("{0}", exceptionMsg);
-			throw new ConnectorIOException(exceptionMsg);
-		}
-		request.setEntity(entity);
-		try (CloseableHttpResponse response = execute(request)){
-			
-			
-			
-			//LOG.ok("Request: {0}", request.toString());
-			//response = execute(request);
-			LOG.ok("Response: {0}", response);
-			processResponseErrors(response);
+	private static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8";
+
+	static final String J_WS_REST_GET_MEMBERS_REQUEST = "WsRestGetMembersRequest";
+	static final String J_WS_REST_FIND_GROUPS_REQUEST = "WsRestFindGroupsRequest";
+	static final String J_WS_REST_FIND_STEMS_REQUEST = "WsRestFindStemsRequest";
+
+	static final String J_WS_QUERY_FILTER = "wsQueryFilter";
+	static final String J_WS_STEM_QUERY_FILTER = "wsStemQueryFilter";
+	static final String J_STEM_QUERY_FILTER_TYPE = "stemQueryFilterType";
+	static final String J_INCLUDE_SUBJECT_DETAIL = "includeSubjectDetail";
+	static final String J_QUERY_FILTER_TYPE = "queryFilterType";
+	static final String J_STEM_NAME = "stemName";
+	static final String J_STEM_NAME_SCOPE = "stemNameScope";
+	static final String J_GROUP_NAME = "groupName";
+
+	static final String J_WS_FIND_GROUPS_RESULTS = "WsFindGroupsResults";
+	static final String J_WS_FIND_STEMS_RESULTS = "WsFindStemsResults";
+	static final String J_WS_GET_MEMBERS_RESULTS = "WsGetMembersResults";
+
+	static final String J_RESULTS = "results";
+	static final String J_STEM_RESULTS = "stemResults";
+	static final String J_GROUP_RESULTS = "groupResults";
+	static final String J_WS_GROUP_LOOKUPS = "wsGroupLookups";
+	private static final String J_RESULT_METADATA = "resultMetadata";
+	private static final String J_SUCCESS = "success";
+
+	static final String J_WS_SUBJECTS = "wsSubjects";
+	static final String J_WS_GROUP = "wsGroup";
+
+	static final String J_UUID = "uuid";
+	static final String J_NAME = "name";
+	static final String J_EXTENSION = "extension";
+	static final String J_SOURCE_ID = "sourceId";
+	static final String J_ID = "id";
+
+	private static final String VAL_T = "T";
+	static final String VAL_FIND_BY_STEM_NAME = "FIND_BY_STEM_NAME";
+	static final String VAL_ALL_IN_SUBTREE = "ALL_IN_SUBTREE";
+
+	private static final String URI_BASE_PATH = "/grouper-ws/servicesRest/json/v2_4_000";
+	private static final String PATH_GROUPS = "/groups";
+	private static final String PATH_STEMS = "/stems";
 
-			if (!parseResult) {
-				return null;
-			}
-			
-			String result = EntityUtils.toString(response.getEntity());
+	GrouperConfiguration configuration;
 
-			LOG.ok("Response body: {0}", result);
-			return new JSONObject(result);
-		} catch (IOException e) {
-			StringBuilder exceptionMsg = new StringBuilder();
-			exceptionMsg.append("Request failed: problem occured during execute request with uri: ")
-					.append(request.getURI()).append(": \n\t").append(e.getLocalizedMessage());
-			LOG.error("{0}", exceptionMsg.toString());
-			throw new ConnectorIOException(exceptionMsg.toString(), e);
-		}
+	Processor(GrouperConfiguration configuration) {
+		this.configuration = configuration;
 	}
 
-	JSONObject callRequest(HttpRequestBase request, Boolean parseResult, String contentType) {
-		// don't log request here - password field !!!
-		//CloseableHttpResponse response = null;
-		LOG.ok("request URI: {0}", request.getURI());
-		request.addHeader("Content-Type", contentType);
-		request.addHeader("Authorization", "Basic " + authEncoding());
-		try (CloseableHttpResponse response = execute(request)){
-			
-			LOG.ok("Response: {0}", response);
+	JSONObject callRequest(HttpEntityEnclosingRequestBase request, JSONObject payload) {
+		request.addHeader("Content-Type", Processor.CONTENT_TYPE_JSON);
+		request.addHeader("Authorization", "Basic " + getAuthEncoded());
+		request.setEntity(new ByteArrayEntity(payload.toString().getBytes(StandardCharsets.UTF_8)));
+		LOG.info("Payload: {0}", payload);      // we don't log the whole request, as it contains the (encoded) password
+		try (CloseableHttpResponse response = execute(request)) {
+			LOG.info("Response: {0}", response);
 			processResponseErrors(response);
 
-			if (!parseResult) {
-				//closeResponse(response);
-				return null;
-			}
-			// DO NOT USE getEntity() TWICE!!!
 			String result = EntityUtils.toString(response.getEntity());
-			//closeResponse(response);
-			LOG.ok("Response body: {0}", result);
+			LOG.info("Response body: {0}", result);
 			return new JSONObject(result);
 		} catch (IOException e) {
-			StringBuilder exceptionMsg = new StringBuilder();
-			exceptionMsg.append("Request failed: problem occured during execute request with uri: ")
-					.append(request.getURI()).append(": \n\t").append(e.getLocalizedMessage());
-			//closeResponse(response);
-			LOG.error("{0}", exceptionMsg.toString());
-			throw new ConnectorIOException(exceptionMsg.toString(), e);
+			String msg = "Request failed: problem occurred during execute request with uri: " + request.getURI() + ": \n\t" + e.getLocalizedMessage();
+			LOG.error("{0}", msg);
+			throw new ConnectorIOException(msg, e);
 		}
 	}
 
-	String authEncoding() {
+	private String getAuthEncoded() {
 		String username = configuration.getUsername();
-		String password = configuration.getStringPassword();
+		String password = configuration.getPasswordPlain();
 		if (username == null || username.equals("")) {
-			LOG.error("Authentication failed: Username is not provided.");
-			throw new InvalidCredentialException("Authentication failed: Username is not provided.");
+			String msg = "Authentication failed: No user name specified";
+			LOG.error("{0}", msg);
+			throw new InvalidCredentialException(msg);
 		}
 		if (password == null || password.equals("")) {
-			LOG.error("Authentication failed: Password is not provided.");
-			throw new InvalidPasswordException("Authentication failed: Password is not provided.");
+			String msg = "Authentication failed: No password specified";
+			LOG.error("{0}", msg);
+			throw new InvalidPasswordException(msg);
 		}
-		StringBuilder nameAndPasswd = new StringBuilder();
-		nameAndPasswd.append(username).append(":").append(password);
-		// String nameAndPasswd = "administrator:training"
-		String encoding = Base64.encodeBase64String(nameAndPasswd.toString().getBytes());
-		return encoding;
+		return Base64.encodeBase64String((username + ":" + password).getBytes());
 	}
 
-	CloseableHttpResponse execute(HttpUriRequest request) {
+	private CloseableHttpResponse execute(HttpUriRequest request) {
 		try {
 			HttpClientBuilder clientBuilder = HttpClientBuilder.create();
 			if (Boolean.TRUE.equals(configuration.getIgnoreSslValidation())) {
 				SSLContextBuilder sslCtxBuilder = new SSLContextBuilder();
-				sslCtxBuilder.loadTrustMaterial(null, new TrustStrategy() {
-					public boolean isTrusted(X509Certificate[] chain, String authType) {
-						return true;
-					}
-				});
+				sslCtxBuilder.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true);
 				SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslCtxBuilder.build(), NoopHostnameVerifier.INSTANCE);
 				clientBuilder.setSSLSocketFactory(factory);
-				System.out.println("Ignoring SSL validation");
+				LOG.warn("Ignoring SSL validation: avoid this in production");
 			}
 			CloseableHttpClient client = clientBuilder.build();
 			CloseableHttpResponse response = client.execute(request);
-			// print response code:
-			LOG.ok("response code: {0}", String.valueOf(response.getStatusLine().getStatusCode()));
-			// client.close();
+			LOG.ok("response code: {0}", response.getStatusLine().getStatusCode());
 			// DO NOT CLOSE response HERE !!!
 			return response;
 		} catch (IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
-			StringBuilder exceptionMsg = new StringBuilder();
-			exceptionMsg.append("Execution of the request failed: problem occurred during HTTP client execution: \n\t")
-					.append(e.getLocalizedMessage());
-			LOG.error("{0}", exceptionMsg.toString(), e);
-			e.printStackTrace();
-			throw new ConnectorIOException(exceptionMsg.toString());
+			String msg = "Execution of the request failed: problem occurred during HTTP client execution: \n\t" + e.getLocalizedMessage();
+			LOG.error("{0}", msg, e);
+			throw new ConnectorIOException(msg);
 		}
 	}
 
@@ -209,84 +158,48 @@ public boolean isTrusted(X509Certificate[] chain, String authType) {
 	 * method throws the ConnId exception that is the most appropriate match for
 	 * the error.
 	 */
-	void processResponseErrors(CloseableHttpResponse response) {
+	private void processResponseErrors(CloseableHttpResponse response) {
 		int statusCode = response.getStatusLine().getStatusCode();
 		if (statusCode >= 200 && statusCode <= 299) {
 			return;
 		}
+
+		if (statusCode == 401 || statusCode == 403) {
+			// sometimes there are binary data in responseBody
+			closeResponse(response);
+			String msg = "HTTP error " + statusCode + " " + response.getStatusLine().getReasonPhrase() + " : Authentication failure.";
+			LOG.error("{0}", msg);
+			throw new InvalidCredentialException(msg);
+		}
+
 		String responseBody = null;
 		try {
 			responseBody = EntityUtils.toString(response.getEntity());
 		} catch (IOException e) {
 			LOG.warn("cannot read response body: {0}", e, e);
 		}
-
-		StringBuilder message = new StringBuilder();
-		message.append("HTTP error ").append(statusCode).append(" ").append(response.getStatusLine().getReasonPhrase())
-				.append(" : ").append(responseBody);
-		if (statusCode == 401 || statusCode == 403) {
-			StringBuilder anauthorizedMessage = new StringBuilder(); // response
-																		// body
-																		// of
-																		// status
-																		// code
-																		// 401
-																		// contains
-																		// binary
-																		// data.
-			anauthorizedMessage.append("HTTP error ").append(statusCode).append(" ")
-					.append(response.getStatusLine().getReasonPhrase())
-					.append(" : Provided credentials are incorrect.");
-			closeResponse(response);
-			LOG.error("{0}", anauthorizedMessage.toString());
-			throw new InvalidCredentialException(anauthorizedMessage.toString());
-		}
-		LOG.error("{0}", message.toString());
-		if ((statusCode == 400 || statusCode == 404) && message.toString().contains("already")) {
-			closeResponse(response);
-			LOG.error("{0}", message.toString());
-			throw new AlreadyExistsException(message.toString());
-		}
+		String msg = "HTTP error " + statusCode + " " + response.getStatusLine().getReasonPhrase() + " : " + responseBody;
+		LOG.error("{0}", msg);
+		closeResponse(response);
 		if (statusCode == 400 || statusCode == 405 || statusCode == 406) {
-			closeResponse(response);
-			LOG.error("{0}", message.toString());
-			throw new ConnectorIOException(message.toString());
-		}
-		if (statusCode == 402 || statusCode == 407) {
-			closeResponse(response);
-			LOG.error("{0}", message.toString());
-			throw new PermissionDeniedException(message.toString());
-		}
-		if (statusCode == 404 || statusCode == 410) {
-			closeResponse(response);
-			LOG.error("{0}", message.toString());
-			throw new UnknownUidException(message.toString());
-		}
-		if (statusCode == 408) {
-			closeResponse(response);
-			LOG.error("{0}", message.toString());
-			throw new OperationTimeoutException(message.toString());
-		}
-		if (statusCode == 412) {
-			closeResponse(response);
-			LOG.error("{0}", message.toString());
-			throw new PreconditionFailedException(message.toString());
-		}
-		if (statusCode == 418) {
-			closeResponse(response);
-			LOG.error("{0}", message.toString());
-			throw new UnsupportedOperationException("Sorry, no coffee: " + message.toString());
+			throw new ConnectorIOException(msg);
+		} else if (statusCode == 402 || statusCode == 407) {
+			throw new PermissionDeniedException(msg);
+		} else if (statusCode == 404 || statusCode == 410) {
+			throw new UnknownUidException(msg);
+		} else if (statusCode == 408) {
+			throw new OperationTimeoutException(msg);
+		} else if (statusCode == 412) {
+			throw new PreconditionFailedException(msg);
+		} else if (statusCode == 418) {
+			throw new UnsupportedOperationException("Sorry, no coffee: " + msg);
+		} else {
+			throw new ConnectorException(msg);
 		}
-
-		closeResponse(response);
-		LOG.error("{0}", message.toString());
-		throw new ConnectorException(message.toString());
 	}
 
-	void closeResponse(CloseableHttpResponse response) {
+	private void closeResponse(CloseableHttpResponse response) {
 		// to avoid pool waiting
-		if (response == null)
-			return;
 		try {
 			response.close();
 		} catch (IOException e) {
@@ -294,138 +207,43 @@ void closeResponse(CloseableHttpResponse response) {
 		}
 	}
 
-	// filter json objects by substring:
-	JSONArray substringFiltering(JSONArray inputJsonArray, String attrName, String subValue) {
-		JSONArray jsonArrayOut = new JSONArray();
-		// String attrName = attribute.getName().toString();
-		// LOGGER.info("\n\tSubstring filtering: {0} ({1})", attrName,
-		// subValue);
-		for (int i = 0; i < inputJsonArray.length(); i++) {
-			JSONObject jsonObject = inputJsonArray.getJSONObject(i);
-			if (!jsonObject.has(attrName)) {
-				LOG.warn("\n\tProcessing JSON Object does not contain attribute {0}.", attrName);
-				return null;
-			}
-			if (jsonObject.has(attrName) && (jsonObject.get(attrName)).toString().contains(subValue)) {
-				// LOG.ok("value: {0}, subValue: {1} - MATCH: {2}",
-				// jsonObject.get(attrName).toString(), subValue, "YES");
-				jsonArrayOut.put(jsonObject);
-			}
-			// else LOG.ok("value: {0}, subValue: {1} - MATCH: {2}",
-			// jsonObject.getString(attrName), subValue, "NO");
-		}
-		return jsonArrayOut;
-	}
-
-	// method called when attribute of query filter is null:
-	void throwNullAttrException(Filter query) {
-		StringBuilder exceptionMsg = new StringBuilder();
-		exceptionMsg
-				.append("Get operation failed: problem occurred because of not provided attribute of query filter: ")
-				.append(query);
-		LOG.error("{0}", exceptionMsg.toString());
-		throw new InvalidAttributeValueException(exceptionMsg.toString());
-	}
-
-	// create uri from base host:
-	URIBuilder getURIBuilder() {
+	private URIBuilder getUriBuilderRelative(String path) {
 		try {
 			URIBuilder uri = new URIBuilder(configuration.getBaseUrl());
-			uri.setPath(URI_BASE_PATH);
+			uri.setPath(URI_BASE_PATH + path);
 			return uri;
 		} catch (URISyntaxException e) {
 			throw new IllegalStateException(e.getMessage(), e);     // todo
 		}
 	}
 
-	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-	<T> T addAttr(ConnectorObjectBuilder builder, String attrName, T attrVal) {
-		if (attrVal != null) {
-			builder.addAttribute(attrName, attrVal);
-		}
-		return attrVal;
-	}
-
-	String getStringAttr(Set<Attribute> attributes, String attrName) throws InvalidAttributeValueException {
-		return getAttr(attributes, attrName, String.class);
-	}
-
-	<T> T getAttr(Set<Attribute> attributes, String attrName, Class<T> type)
-			throws InvalidAttributeValueException {
-		return getAttr(attributes, attrName, type, null);
+	URIBuilder getUriBuilderForGroups() {
+		return getUriBuilderRelative(PATH_GROUPS);
 	}
 
-
-	@SuppressWarnings("unchecked")
-	private <T> T getAttr(Set<Attribute> attributes, String attrName, Class<T> type, T defaultVal)
-			throws InvalidAttributeValueException {
-		for (Attribute attr : attributes) {
-			if (attrName.equals(attr.getName())) {
-				List<Object> vals = attr.getValue();
-				if (vals == null || vals.isEmpty()) {
-					// set empty value
-					return null;
-				}
-				if (vals.size() == 1) {
-					Object val = vals.get(0);
-					if (val == null) {
-						// set empty value
-						return null;
-					}
-					if (type.isAssignableFrom(val.getClass())) {
-						return (T) val;
-					}
-					StringBuilder exceptionMsg = new StringBuilder();
-					exceptionMsg.append("Unsupported type ").append(val.getClass()).append(" for attribute ")
-							.append(attrName).append(", value: ").append(val);
-					LOG.error("{0}", exceptionMsg.toString());
-					throw new InvalidAttributeValueException(exceptionMsg.toString());
-				}
-				StringBuilder exceptionMsg = new StringBuilder();
-				exceptionMsg.append("More than one value for attribute ").append(attrName).append(", values: ")
-						.append(vals);
-				LOG.error("{0}", exceptionMsg.toString());
-				throw new InvalidAttributeValueException(exceptionMsg.toString());
-			}
-		}
-		// set default value when attrName not in changed attributes
-		return defaultVal;
+	URIBuilder getUriBuilderForStems() {
+		return getUriBuilderRelative(PATH_STEMS);
 	}
 
-	void getIfExists(JSONObject jsonObj, String attr, ConnectorObjectBuilder builder, boolean isMultiValue) {
-		if (jsonObj.has(attr) && jsonObj.get(attr) != null && !JSONObject.NULL.equals(jsonObj.get(attr))) {
-			if (isMultiValue) {
-				JSONArray attrJSONArray = jsonObj.getJSONArray(attr);
-				if (attrJSONArray != null) {
-					int size = attrJSONArray.length();
-					ArrayList<String> attrStringArray = new ArrayList<String>();
-					for (int i = 0; i < size; i++) {
-						attrStringArray.add(attrJSONArray.get(i).toString());
-					}
-					builder.addAttribute(attr, attrStringArray.toArray());
-				}
-			} else
-				addAttr(builder, attr, jsonObj.get(attr));
-		}
-	}
+	// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-	public void checkSuccess(JSONObject response, String rootName) {
-		Object success = get(response, rootName, RESULT_METADATA, SUCCESS);
-		if (!"T".equals(success)) {
+	void checkSuccess(JSONObject response, String rootName) {
+		Object success = get(response, rootName, J_RESULT_METADATA, J_SUCCESS);
+		if (!VAL_T.equals(success)) {
 			throw new IllegalStateException("Request was not successful: " + success);
 		}
 	}
 
+	@SuppressWarnings("unused")
 	public Object getIfExists(JSONObject object, String... items) {
 		return get(object, false, items);
 	}
 
-	public Object get(JSONObject object, String... items) {
+	Object get(JSONObject object, String... items) {
 		return get(object, true, items);
 	}
 
-	public Object get(JSONObject object, boolean mustExist, String... items) {
+	private Object get(JSONObject object, boolean mustExist, String... items) {
 		if (items.length == 0) {
 			throw new IllegalArgumentException("Empty item path");
 		}
@@ -468,11 +286,12 @@ public Object get(JSONObject object, boolean mustExist, String... items) {
 		}
 	}
 
-	public JSONArray getArray(JSONObject object, String... items) {
+	@SuppressWarnings("unused")
+	JSONArray getArray(JSONObject object, String... items) {
 		return getArray(object, true, items);
 	}
 
-	public JSONArray getArray(JSONObject object, boolean mustExist, String... items) {
+	JSONArray getArray(JSONObject object, boolean mustExist, String... items) {
 		Object rv = get(object, mustExist, items);
 		if (rv == null) {
 			assert !mustExist;
@@ -484,19 +303,18 @@ public JSONArray getArray(JSONObject object, boolean mustExist, String... items)
 		}
 	}
 
-	public ConnectorException processException(Exception e, URIBuilder uriBuilder, final String operationName) {
-		StringBuilder exceptionMsg = new StringBuilder();
-		exceptionMsg.append(operationName).append(" failed: problem occurred during executing URI: ").append(uriBuilder)
-				.append("\n\t").append(e.getLocalizedMessage());
-		LOG.error("{0}", exceptionMsg.toString());
-		return new ConnectorException(exceptionMsg.toString(), e);
+	ConnectorException processException(Exception e, URIBuilder uriBuilder, final String operationName) {
+		String msg = operationName + " failed: problem occurred during executing URI: " + uriBuilder + "\n\t" + e.getMessage();
+		LOG.error("{0}", msg);
+		return new ConnectorException(msg, e);
 	}
 
+	@SuppressWarnings("unused")
 	public boolean isSuccess(JSONObject object) {
-		return "T".equals(getStringOrNull(object, SUCCESS));
+		return VAL_T.equals(getStringOrNull(object, J_SUCCESS));
 	}
 
-	public String getStringOrNull(JSONObject object, String item) {
+	String getStringOrNull(JSONObject object, String item) {
 		if (object.has(item)) {
 			return getString(object, item);
 		} else {
@@ -504,7 +322,7 @@ public String getStringOrNull(JSONObject object, String item) {
 		}
 	}
 
-	public String getString(JSONObject object, String item) {
+	private String getString(JSONObject object, String item) {
 		return (String) get(object, item);  // todo error handling
 	}
 
@@ -512,8 +330,10 @@ boolean groupNameMatches(String name) {
 		if (name == null) {
 			return false;
 		}
-		return groupNameMatches(name, configuration.getGroupIncludePattern()) &&
-				!groupNameMatches(name, configuration.getGroupExcludePattern());
+		String[] includes = configuration.getGroupIncludePattern();
+		String[] excludes = configuration.getGroupExcludePattern();
+		return (includes == null || includes.length == 0 || groupNameMatches(name, includes)) &&
+				!groupNameMatches(name, excludes);
 	}
 
 	private boolean groupNameMatches(String name, String[] patterns) {
diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/StandardGroupProcessor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/StandardGroupProcessor.java
deleted file mode 100644
index 9507c5e..0000000
--- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/StandardGroupProcessor.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*******************************************************************************
- * Copyright 2017 Evolveum
- * 
- * 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.
- ******************************************************************************/
-package com.evolveum.polygon.connector.grouper.rest;
-
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.utils.URIBuilder;
-import org.identityconnectors.framework.common.objects.*;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.net.URISyntaxException;
-
-import static com.evolveum.polygon.connector.grouper.rest.Processor.*;
-
-/**
- * @author surmanek
- * @author mederly
- *
- */
-class StandardGroupProcessor extends AbstractGroupProcessor {
-
-	StandardGroupProcessor(Processor processor) {
-		super(processor);
-	}
-
-	ObjectClassInfoBuilder buildSchema() {
-		ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder();
-
-		builder.setType(ObjectClass.GROUP_NAME);
-		AttributeInfoBuilder extension = new AttributeInfoBuilder(ATTR_EXTENSION, String.class);
-		builder.addAttributeInfo(extension.build());
-
-		return builder;
-	}
-
-	protected void getAllGroups(ResultsHandler handler, OperationOptions options) {
-		URIBuilder uriBuilder = processor.getURIBuilder()
-				.setPath(URI_BASE_PATH + PATH_GROUPS);
-		try {
-			HttpPost request = new HttpPost(uriBuilder.build());
-			JSONObject body = new JSONObject()
-					.put("WsRestGetMembersRequest", new JSONObject()
-							.put("wsGroupLookups", new JSONObject[] { new JSONObject()
-									.put("groupName", getConfiguration().getSuperGroup()) })
-							.put("includeSubjectDetail", true)
-							.put("memberFilter", "Immediate"));
-			executeFindGroupsAsMembersResponse(request, body, handler);
-		} catch (RuntimeException | URISyntaxException e) {
-			throw processor.processException(e, uriBuilder, "Get all groups");
-		}
-	}
-
-	@Override
-	protected ObjectClass getObjectClass() {
-		return ObjectClass.GROUP;
-	}
-}
diff --git a/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties b/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties
index 1fe1993..572bfe4 100644
--- a/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties
+++ b/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties
@@ -1,8 +1,47 @@
+#
+# Copyright (c) 2019 Evolveum
+#
+# 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.
+#
+
+baseUrl.display=Base URL
+baseUrl.help=URL on which the Grouper REST service can be accessed. An example: https://localhost:9443.
+
 username.display=Username
-username.help=Please provide the administrator user name for the jira enabled service you are logging into
+username.help=Name of the user that is used to access the Grouper REST service.
 
 password.display=Password
-password.help=Please provide the administrator password used to connect to the jira enabled service
+password.help=Password of the user that is used to access the Grouper REST service.
 
-baseUrl.display=Base URL
-baseUrl.help=Please provide the base url for access to resources (e.g. host:port/rest/api/2/user)
\ No newline at end of file
+exportStem.display=Root stem
+exportStem.help=The stem that is to be exported (visible) to this connector. The default is ":" (the whole tree).
+
+groupIncludePattern.display=Groups to include
+groupIncludePattern.help=Groups that should be visible to this connector. Specify them using regular expressions like "ref:.*". If nothing is specified, all groups under root stem are included.
+
+groupExcludePattern.display=Groups to exclude
+groupExcludePattern.help=Groups that should not be visible to this connector. Specify them using regular expressions like ".*_(includes|excludes|systemOfRecord|systemOfRecordAndIncludes)".
+
+ignoreSslValidation.display=Ignore SSL validation
+ignoreSslValidation.help=Whether to ignore SSL validation issues when connecting to the Grouper REST service. Do not use in production.
+
+subjectSource.display=Subject source
+subjectSource.help=The source of subjects that will be visible by this connector.
+
+testStem.display=Test stem
+testStem.help=Stem whose accessibility is checked during Test connection operation (if specified).
+
+testGroup.display=Test group
+testGroup.help=Group whose accessibility is checked during Test connection operation (if specified).
+
+GrouperConnector.rest.display=Grouper connector
\ No newline at end of file
diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/AbstractTest.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/AbstractTest.java
new file mode 100644
index 0000000..e8d405e
--- /dev/null
+++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/AbstractTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2019 Evolveum
+ *
+ * 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.
+ */
+
+package com.evolveum.polygon.connector.grouper.test;
+
+import com.evolveum.polygon.connector.grouper.rest.GrouperConfiguration;
+import com.evolveum.polygon.connector.grouper.rest.GrouperConnector;
+import org.identityconnectors.common.security.GuardedString;
+import org.identityconnectors.framework.common.objects.ConnectorObject;
+import org.identityconnectors.framework.common.objects.OperationOptions;
+import org.identityconnectors.framework.common.objects.SearchResult;
+import org.identityconnectors.framework.spi.SearchResultsHandler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Superclass for connector tests. These tests require running Grouper instance, with a group of TEST_GROUP having
+ * a single user TEST_USER in subject source SUBJECT_SOURCE.
+ */
+class AbstractTest {
+
+	// Test configuration
+	static final String TEST_USER = "banderson";
+	static final String TEST_GROUP = "etc:sysadmingroup";
+
+	// Connector configuration
+	private static final String BASE_URL = "https://192.168.56.101:9443";
+	private static final String ADMIN_USERNAME = TEST_USER;
+	private static final String ADMIN_PASSWORD = "password";
+	private static final String EXPORT_STEM = "etc";
+	private static final String[] GROUP_INCLUDE_PATTERN = { ".*" };
+	private static final String[] GROUP_EXCLUDE_PATTERN = { ".*_(includes|excludes|systemOfRecord|systemOfRecordAndIncludes)" };
+	private static final String SUBJECT_SOURCE = "ldap";
+	private static final String TEST_STEM = ":";
+
+	final GrouperConnector grouperConnector = new GrouperConnector();
+	final OperationOptions options = new OperationOptions(new HashMap<>());
+
+	final ArrayList<ConnectorObject> results = new ArrayList<>();
+	SearchResultsHandler handler = new SearchResultsHandler() {
+		@Override
+		public boolean handle(ConnectorObject connectorObject) {
+			results.add(connectorObject);
+			return true;
+		}
+
+		@Override
+		public void handleResult(SearchResult result) {
+		}
+	};
+	
+	GrouperConfiguration getConfiguration() {
+		GrouperConfiguration config = new GrouperConfiguration();
+		config.setBaseUrl(BASE_URL);
+		config.setUsername(ADMIN_USERNAME);
+		config.setPassword(new GuardedString(ADMIN_PASSWORD.toCharArray()));
+		config.setIgnoreSslValidation(true);
+		config.setExportStem(EXPORT_STEM);
+		config.setGroupIncludePattern(GROUP_INCLUDE_PATTERN);
+		config.setGroupExcludePattern(GROUP_EXCLUDE_PATTERN);
+		config.setSubjectSource(SUBJECT_SOURCE);
+		config.setTestStem(TEST_STEM);
+		config.setTestGroup(TEST_GROUP);
+		return config;
+	}
+}
diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/AccountTests.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/AccountTests.java
deleted file mode 100644
index a5e190a..0000000
--- a/src/test/java/com/evolveum/polygon/connector/grouper/test/AccountTests.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.evolveum.polygon.connector.grouper.test;
-/*******************************************************************************
- * Copyright 2019 Evolveum
- * 
- * 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.
- ******************************************************************************/
-
-import org.identityconnectors.framework.common.objects.*;
-import org.identityconnectors.framework.common.objects.filter.AttributeFilter;
-import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
-import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
-import org.testng.annotations.Test;
-
-import static org.testng.AssertJUnit.assertEquals;
-
-/**
- * @author surmanek
- * @author mederly
- *
- */
-public class AccountTests extends GrouperTestHelper {
-	
-	@Test(priority = 1)
-	public void initTest() {
-		grouperConnector.init(getConfiguration());
-		cleanUp();
-	}
-	
-	@Test(priority = 3)
-	public void schemaTest() {
-		grouperConnector.schema();
-	}
-
-	@Test(priority = 3)
-	public void testTest() {
-		grouperConnector.test();
-	}
-
-	@Test(priority = 4)
-	public void nameEqualsFilteringForAccountsTest() {
-		// filtering:
-		results.clear();
-		AttributeFilter filter = (EqualsFilter) FilterBuilder
-				.equalTo(AttributeBuilder.build(Name.NAME, "banderson"));
-
-		grouperConnector.executeQuery(accountObjectClass, filter, handler, options);
-		assertEquals("Wrong # of users retrieved", results.size(), 1);
-		ConnectorObject user = results.get(0);
-		System.out.println("Found user: " + user);
-	}
-	
-	@Test(priority = 6)
-	public void listingAccountsTest() {
-		results.clear();
-		grouperConnector.executeQuery(accountObjectClass, null, handler, options);
-
-		assertEquals("Wrong # of users retrieved", results.size(), 1);
-		ConnectorObject user = results.get(0);
-		System.out.println("Found user: " + user);
-	}
-
-	
-	@Test(priority = 20)
-	public void disposeTest() {
-		 grouperConnector.dispose();
-	}
-	
-	private void cleanUp() {
-		results.clear();
-	}
-}
diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/PlainGroupTests.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTest.java
similarity index 57%
rename from src/test/java/com/evolveum/polygon/connector/grouper/test/PlainGroupTests.java
rename to src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTest.java
index b9722e3..4972329 100644
--- a/src/test/java/com/evolveum/polygon/connector/grouper/test/PlainGroupTests.java
+++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTest.java
@@ -1,68 +1,68 @@
-package com.evolveum.polygon.connector.grouper.test;
-/*******************************************************************************
- * Copyright 2019 Evolveum
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); 
- * you may not use this file except in compliance with the License.  
+/*
+ * Copyright (c) 2019 Evolveum
+ *
+ * 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
- * 
+ *
+ *     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.
- ******************************************************************************/
+ * 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.
+ */
+
+package com.evolveum.polygon.connector.grouper.test;
 
-import com.evolveum.polygon.connector.grouper.rest.PlainGroupProcessor;
+import com.evolveum.polygon.connector.grouper.rest.GroupProcessor;
 import org.identityconnectors.framework.common.objects.*;
 import org.identityconnectors.framework.common.objects.filter.AttributeFilter;
 import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
 import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
 import org.testng.annotations.Test;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
+import static com.evolveum.polygon.connector.grouper.rest.GroupProcessor.ATTR_NAME;
+import static com.evolveum.polygon.connector.grouper.rest.GroupProcessor.ATTR_UUID;
 import static org.identityconnectors.framework.common.objects.OperationOptions.OP_ATTRIBUTES_TO_GET;
 import static org.testng.AssertJUnit.assertEquals;
 
 /**
- * @author surmanek
- * @author mederly
- *
+ * Tests the group object class. See the superclass for the environment needed.
  */
-public class PlainGroupTests extends GrouperTestHelper {
+public class GroupTest extends AbstractTest {
 
-	private static final ObjectClass PLAIN_GROUP = new ObjectClass(PlainGroupProcessor.OBJECT_CLASS_NAME);
+	private static final ObjectClass OC_GROUP = new ObjectClass(GroupProcessor.OBJECT_CLASS_NAME);
 
 	private String uuid;
 
 	@Test(priority = 1)
-	public void initTest() {
+	public void initialization() {
 		grouperConnector.init(getConfiguration());
-		cleanUp();
 	}
 	
 	@Test(priority = 2)
-	public void schemaTest() {
+	public void testSchema() {
 		grouperConnector.schema();
 	}
 
 	@Test(priority = 3)
-	public void testTest() {
+	public void testTestOperation() {
 		grouperConnector.test();
 	}
 
 	@Test(priority = 4)
-	public void findByGroupName() {
-		// filtering:
+	public void testFindByGroupName() {
 		results.clear();
 		AttributeFilter filter = (EqualsFilter) FilterBuilder
-				.equalTo(AttributeBuilder.build(Name.NAME, "etc:sysadmingroup"));
+				.equalTo(AttributeBuilder.build(ATTR_NAME, TEST_GROUP));
 
-		grouperConnector.executeQuery(PLAIN_GROUP, filter, handler, options);
+		grouperConnector.executeQuery(OC_GROUP, filter, handler, options);
 		assertEquals("Wrong # of groups retrieved", results.size(), 1);
 		ConnectorObject group = results.get(0);
 		System.out.println("Found group: " + group);
@@ -70,60 +70,57 @@ public void findByGroupName() {
 	}
 	
 	@Test(priority = 10)
-	public void findByGroupNameWithMembers() {
-		// filtering:
+	public void testFindByGroupNameWithMembers() {
 		results.clear();
 		AttributeFilter filter = (EqualsFilter) FilterBuilder
-				.equalTo(AttributeBuilder.build(Name.NAME, "etc:sysadmingroup"));
-
-		grouperConnector.executeQuery(PLAIN_GROUP, filter, handler, getMembersOptions());
+				.equalTo(AttributeBuilder.build(ATTR_NAME, TEST_GROUP));
+		
+		grouperConnector.executeQuery(OC_GROUP, filter, handler, getMembersOptions());
 		assertEquals("Wrong # of groups retrieved", results.size(), 1);
 		ConnectorObject group = results.get(0);
 		System.out.println("Found group: " + group);
 		List<String> members = getMembers(group);
-		assertEquals("Wrong members", Collections.singletonList("banderson"), members);
+		assertEquals("Wrong members", Collections.singletonList(TEST_USER), members);
 	}
-
+	
 	@Test(priority = 12)
-	public void findByGroupUuid() {
-		// filtering:
+	public void testFindByGroupUuid() {
 		results.clear();
 		AttributeFilter filter = (EqualsFilter) FilterBuilder
-				.equalTo(AttributeBuilder.build(Uid.NAME, uuid));
+				.equalTo(AttributeBuilder.build(ATTR_UUID, uuid));
 
-		grouperConnector.executeQuery(PLAIN_GROUP, filter, handler, options);
+		grouperConnector.executeQuery(OC_GROUP, filter, handler, options);
 		assertEquals("Wrong # of groups retrieved", results.size(), 1);
 		ConnectorObject group = results.get(0);
 		System.out.println("Found group: " + group);
 	}
 
 	@Test(priority = 13)
-	public void findByGroupUuidWihMembers() {
-		// filtering:
+	public void testFindByGroupUuidWihMembers() {
 		results.clear();
 		AttributeFilter filter = (EqualsFilter) FilterBuilder
-				.equalTo(AttributeBuilder.build(Uid.NAME, uuid));
+				.equalTo(AttributeBuilder.build(ATTR_UUID, uuid));
 
-		grouperConnector.executeQuery(PLAIN_GROUP, filter, handler, getMembersOptions());
+		grouperConnector.executeQuery(OC_GROUP, filter, handler, getMembersOptions());
 		assertEquals("Wrong # of groups retrieved", results.size(), 1);
 		ConnectorObject group = results.get(0);
 		System.out.println("Found group: " + group);
-		assertEquals("Wrong members", Collections.singletonList("banderson"), getMembers(group));
+		assertEquals("Wrong members", Collections.singletonList(TEST_USER), getMembers(group));
 	}
 
 	@Test(priority = 14)
-	public void allGroups() {
+	public void testGetAllGroups() {
 		results.clear();
-		grouperConnector.executeQuery(PLAIN_GROUP, null, handler, options);
+		grouperConnector.executeQuery(OC_GROUP, null, handler, options);
 		for (ConnectorObject group : results) {
 			System.out.println("Found group: " + group);
 		}
 	}
 
 	@Test(priority = 16)
-	public void allGroupsWithMembers() {
+	public void testGetAllGroupsWithMembers() {
 		results.clear();
-		grouperConnector.executeQuery(PLAIN_GROUP, null, handler, getMembersOptions());
+		grouperConnector.executeQuery(OC_GROUP, null, handler, getMembersOptions());
 		for (ConnectorObject group : results) {
 			System.out.println("Found group: " + group);
 		}
@@ -134,19 +131,15 @@ public void dispose() {
 		 grouperConnector.dispose();
 	}
 	
-	private void cleanUp() {
-		results.clear();
-	}
-
 	private OperationOptions getMembersOptions() {
 		HashMap<String, Object> map = new HashMap<>();
-		map.put(OP_ATTRIBUTES_TO_GET, new String[] { PlainGroupProcessor.ATTR_MEMBER });
+		map.put(OP_ATTRIBUTES_TO_GET, new String[] { GroupProcessor.ATTR_MEMBER });
 		return new OperationOptions(map);
 	}
 
 	private List<String> getMembers(ConnectorObject group) {
-		Attribute attribute = group.getAttributeByName(PlainGroupProcessor.ATTR_MEMBER);
+		Attribute attribute = group.getAttributeByName(GroupProcessor.ATTR_MEMBER);
 		//noinspection unchecked
-		return attribute != null ? (List<String>) (List) attribute.getValue() : Collections.<String>emptyList();
+		return attribute != null ? (List<String>) (List) attribute.getValue() : Collections.emptyList();
 	}
-}
+}
\ No newline at end of file
diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/GrouperTestHelper.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/GrouperTestHelper.java
deleted file mode 100644
index 450090c..0000000
--- a/src/test/java/com/evolveum/polygon/connector/grouper/test/GrouperTestHelper.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package com.evolveum.polygon.connector.grouper.test;
-/*******************************************************************************
- * Copyright 2017 Evolveum
- * 
- * 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.
- ******************************************************************************/
-
-import com.evolveum.polygon.connector.grouper.rest.GrouperConfiguration;
-import com.evolveum.polygon.connector.grouper.rest.GrouperConnector;
-import org.identityconnectors.common.logging.Log;
-import org.identityconnectors.common.security.GuardedString;
-import org.identityconnectors.framework.common.objects.ConnectorObject;
-import org.identityconnectors.framework.common.objects.ObjectClass;
-import org.identityconnectors.framework.common.objects.OperationOptions;
-import org.identityconnectors.framework.common.objects.SearchResult;
-import org.identityconnectors.framework.spi.SearchResultsHandler;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-
-/**
- * @author surmanek
- * @author mederly
- */
-public class GrouperTestHelper {
-	
-	private static final String BASE_URL = "https://192.168.56.101:9443";
-	private static final String ADMIN_USERNAME = "banderson";
-	private static final String ADMIN_PASSWORD = "password";
-	private static final String SUPER_GROUP = "etc:sysadmingroup";
-	private static final String ROOT_STEM = "etc";
-	private static final String SUBJECT_SOURCE = "ldap";
-
-	protected final GrouperConnector grouperConnector = new GrouperConnector();
-	protected final OperationOptions options = new OperationOptions(new HashMap<String, Object>());
-	protected final ObjectClass accountObjectClass = ObjectClass.ACCOUNT;
-
-	protected final ArrayList<ConnectorObject> results = new ArrayList<>();
-	protected SearchResultsHandler handler = new SearchResultsHandler() {
-		@Override
-		public boolean handle(ConnectorObject connectorObject) {
-			results.add(connectorObject);
-			return true;
-		}
-
-		@Override
-		public void handleResult(SearchResult result) {
-		}
-	};
-	
-	//group variables:
-	protected static final ObjectClass groupObjectClass = ObjectClass.GROUP;
-	protected final ArrayList<ConnectorObject> groupResults = new ArrayList<>();
-	protected SearchResultsHandler groupHandler = new SearchResultsHandler() {
-		@Override
-		public boolean handle(ConnectorObject connectorObject) {
-			groupResults.add(connectorObject);
-			return true;
-		}
-
-		@Override
-		public void handleResult(SearchResult result) {
-		}
-	};
-	
-	//project variables:
-	protected static final ObjectClass projectObjectClass = new ObjectClass("PROJECT");
-	protected final ArrayList<ConnectorObject> projectResults = new ArrayList<>();
-	protected SearchResultsHandler projectHandler = new SearchResultsHandler() {
-		@Override
-		public boolean handle(ConnectorObject connectorObject) {
-			projectResults.add(connectorObject);
-			return true;
-		}
-
-		@Override
-		public void handleResult(SearchResult result) {
-		}
-	};
-	
-	protected GrouperConfiguration getConfiguration() {
-		GrouperConfiguration config = new GrouperConfiguration();
-		config.setBaseUrl(BASE_URL);
-		config.setUsername(ADMIN_USERNAME);
-		config.setPassword(new GuardedString(ADMIN_PASSWORD.toCharArray()));
-		config.setSuperGroup(SUPER_GROUP);
-		config.setGroupIncludePattern(new String[] { ".*" });
-		config.setGroupExcludePattern(new String[] { ".*_(includes|excludes|systemOfRecord|systemOfRecordAndIncludes)" });
-		config.setIgnoreSslValidation(true);
-		config.setSubjectSource(SUBJECT_SOURCE);
-		config.setExportStem("ref");
-		return config;
-	}
-}
diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/StandardGroupTests.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/StandardGroupTests.java
deleted file mode 100644
index e906a20..0000000
--- a/src/test/java/com/evolveum/polygon/connector/grouper/test/StandardGroupTests.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.evolveum.polygon.connector.grouper.test;
-/*******************************************************************************
- * Copyright 2019 Evolveum
- * 
- * 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.
- ******************************************************************************/
-
-import org.identityconnectors.framework.common.objects.*;
-import org.identityconnectors.framework.common.objects.filter.AttributeFilter;
-import org.identityconnectors.framework.common.objects.filter.EqualsFilter;
-import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
-import org.testng.annotations.Test;
-
-import static org.testng.AssertJUnit.assertEquals;
-
-/**
- * @author surmanek
- * @author mederly
- *
- */
-public class StandardGroupTests extends GrouperTestHelper {
-
-	private String uuid;
-
-	@Test(priority = 1)
-	public void initTest() {
-		grouperConnector.init(getConfiguration());
-		cleanUp();
-	}
-	
-	@Test(priority = 2)
-	public void schemaTest() {
-		grouperConnector.schema();
-	}
-
-	@Test(priority = 3)
-	public void testTest() {
-		grouperConnector.test();
-	}
-
-	@Test(priority = 4)
-	public void findByGroupName() {
-		// filtering:
-		results.clear();
-		AttributeFilter filter = (EqualsFilter) FilterBuilder
-				.equalTo(AttributeBuilder.build(Name.NAME, "etc:sysadmingroup"));
-
-		grouperConnector.executeQuery(ObjectClass.GROUP, filter, handler, options);
-		assertEquals("Wrong # of groups retrieved", results.size(), 1);
-		ConnectorObject group = results.get(0);
-		System.out.println("Found group: " + group);
-		uuid = group.getUid().getUidValue();
-	}
-	
-	@Test(priority = 5)
-	public void findByGroupUuid() {
-		// filtering:
-		results.clear();
-		AttributeFilter filter = (EqualsFilter) FilterBuilder
-				.equalTo(AttributeBuilder.build(Uid.NAME, uuid));
-
-		grouperConnector.executeQuery(ObjectClass.GROUP, filter, handler, options);
-		assertEquals("Wrong # of groups retrieved", results.size(), 1);
-		ConnectorObject group = results.get(0);
-		System.out.println("Found group: " + group);
-	}
-
-	@Test(priority = 6)
-	public void allGroupsTest() {
-		results.clear();
-		grouperConnector.executeQuery(ObjectClass.GROUP, null, handler, options);
-		// most probably here will be no groups, as etc:sysadmingroup has no direct group members
-		for (ConnectorObject group : results) {
-			System.out.println("Found group: " + group);
-		}
-	}
-
-	
-	@Test(priority = 20)
-	public void disposeTest() {
-		 grouperConnector.dispose();
-	}
-	
-	private void cleanUp() {
-		results.clear();
-	}
-}