![JAR search and dependency download from the Maven repository](/logo.png)
org.identityconnectors.contract.data.GroovyDataProvider Maven / Gradle / Ivy
/*
* ====================
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License("CDDL") (the "License"). You may not use this file
* except in compliance with the License.
*
* You can obtain a copy of the License at
* http://opensource.org/licenses/cddl1.php
* See the License for the specific language governing permissions and limitations
* under the License.
*
* When distributing the Covered Code, include this CDDL Header Notice in each file
* and include the License file at http://opensource.org/licenses/cddl1.php.
* If applicable, add the following below this CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* Portions Copyrighted 2013 ConnId
*/
package org.identityconnectors.contract.data;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import groovy.util.ConfigObject;
import groovy.util.ConfigSlurper;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.identityconnectors.common.StringUtil;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.contract.data.groovy.Get;
import org.identityconnectors.contract.data.groovy.Lazy;
import org.identityconnectors.contract.data.groovy.Random;
import org.identityconnectors.contract.exceptions.ObjectNotFoundException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.test.common.TestHelpers;
/**
*
* Default implementation of {@link DataProvider}. It uses ConfigSlurper from
* Groovy to parse the property file.
* The groovy files are read as classpath resources using following paths :
*
* loader.getResource(prefix + "/config/config.groovy")
* loader.getResource(prefix + "/config/" + cfg + "/config.groovy")
optionally where cfg is passed
* configuration
* -
loader.getResource(prefix + "/config-private/config.groovy")
* -
loader.getResource(prefix + "/config-private/" + cfg + "/config.groovy")
optionally where cfg is
* passed configuration
*
* where prefix is FQN of your connector set as "connectorName" system property.
* Note: If two property files contain the same property name, the value from
* the latter file the list overrides the others. I.e. the last file
* from the list has the greatest chance to propagate its values to the final
* configuration.
*
*
*
* Lazy.random("####")
is used for generating random strings, in
* case numeric object is needed, use for instance
* Lazy.random("####", Long.class)
to get a Long object with
* random value.
*
*
*
* Snapshot generation to output file -- this feature is
* implemented by method {@link GroovyDataProvider#flatten(Object)}. Snapshot
* generating works in one direction, but the snapshot itself cannot be directly
* used as an input to next testing.
*
*
* Snapshots -- usage: add switch -Dtest.parameters.outFile=generated.properties
* as an ANT parameter. The result snapshot file will be included in the connector's directory.
*
*
* Note: snapshots for now support basic types such as Lazy, String. Other objects
* will be converted with toString() method to the output.
*
*
*
* Snapshots of queried properties -- usage:
*
* add switch -Dtest.parameters.outQueriedFile=dumpedq.properties
* as an ANT parameter. The result snapshot file will be included in the connector's directory.
*
*
* default values -- these values reside in file bootstrap.groovy.
* When the property foo.bar.boo is queried the following queries are executed:
*
* 1) foo.bar.boo
* 2) bar.boo
* 3) boo
*
* In case none of these queries succeed, the default value is used based on the type of the query.
*
*
* isMultivalue boolean property -- is passed in get(...) methods
* of GroovyDataProvider. It has influence on default values generated, when property is missing.
*
*
* @author David Adam
* @author Zdenek Louzensky
*/
public class GroovyDataProvider implements DataProvider {
private static final int SINGLE_VALUE_MARKER = -1;
private static final String ARRAY_MARKER = "array";
protected static final String PROPERTY_SEPARATOR = ".";
/** boostrap.groovy contains default values that are returned when the property is not found */
private static final String BOOTSTRAP_FILE_NAME = "bootstrap.groovy";
/** prefix of default values that are multi */
public static final String MULTI_VALUE_TYPE_PREFIX = "multi";
/** holds the parsed config file */
private ConfigObject configObject;
/** cache for resolved values */
private final Map cache = new HashMap<>();
private final ConfigSlurper cs = null != System.getProperty("environment")
? new ConfigSlurper(System.getProperty("environment")) : new ConfigSlurper();
private static final Log LOG = Log.getLog(GroovyDataProvider.class);
/* **** for snapshot generating **** */
/** command line switch for snapshots */
private final String PARAM_PROPERTY_OUT_FILE = "test.parameters.outFile";
/** command line switch for creating queried properties' dump */
private final String PARAM_QUERIED_PROPERTY_OUT_FILE = "test.parameters.outQueriedFile";
/** buffer for queried properties log */
private StringBuilder dumpBuffer = null;
/** buffer for queried properties -- that were not found -- log */
private StringBuilder dumpBufferNotFound = null;
/** default values generated for */
private StringBuilder dumpBufferDefaultVal = null;
/** output file for concatenated snapshots */
private File _propertyOutFile = null;
/** output file for queried properties dump */
private File _queriedPropsOutFile = null;
static final String ASSIGNMENT_MARK = "=";
private final String FOUND_MSG = "found";
/** Turn on debugging prefixes in parsing. Output: System.out */
private final boolean DEBUG_ON = false;
private final String EMPTY_PREFIX = "";
/**
* default constructor
*/
public GroovyDataProvider() {
this(System.getProperty("connectorName"));
}
public GroovyDataProvider(String connectorName) {
if (StringUtil.isBlank(connectorName)) {
throw new IllegalArgumentException(
"To run contract tests, you must specify valid [connectorName] "
+ "system property with the value equal to FQN of your connector class, "
+ "or use GroovyDataProvider(String connectorName) constructor");
}
initSnapshot();
initQueriedPropsDump();
// init
configObject = doBootstrap();
ConfigObject projectConfig = GroovyConfigReader.loadResourceConfiguration(connectorName, getClass().
getClassLoader());
configObject = mergeConfigObjects(configObject, projectConfig);
checkJarDependencies(this, this.getClass().getClassLoader());
}
/**
* check the presence of expected JAR's on the classpath
*
* The test configuration file should contain a definition of required JARs.
* The key of the map is the classname that's needed, the value is
* information included in the error message (supposed to be the jar's
* name).
*
* It could be example:
*
* testsuite.requiredClasses = [ 'com.mysql.jdbc.Driver' : 'Connector/J
* 5.0.8 (mysql-connector-java-5.0.8-bin.jar)' ]
*
* where 'com.mysql.jdbc.Driver' is the awaited class, and 'Connector/J...'
* is the information message describing the JAR, where the class resides.
*/
private static void checkJarDependencies(DataProvider dp, ClassLoader classLoader) {
final String PROP_REQUIRED_CLASSES = "requiredClasses";
Object o = null;
try {
o = dp.getTestSuiteAttribute(PROP_REQUIRED_CLASSES);
} catch (ObjectNotFoundException ex) {
// if property testsuite.requiredClasses is undefined skip checking JARs.
return;
}
if (o instanceof Map, ?>) {
@SuppressWarnings("unchecked")
Map map = (Map) o;
map.entrySet().forEach(entry -> {
try {
Class.forName(entry.getKey(), false, classLoader);
} catch (ClassNotFoundException e) {
fail(String.format("Missing library from classpath: '%s'", entry.getValue()));
}
});
}
}
private void initQueriedPropsDump() {
// get snapshot output file, if provided
String pOut = System.getProperty(PARAM_QUERIED_PROPERTY_OUT_FILE);
if (StringUtil.isNotBlank(pOut)) {
try {
_queriedPropsOutFile = new File(pOut);
if (!_queriedPropsOutFile.exists()) {
_queriedPropsOutFile.createNewFile();
}
if (!_queriedPropsOutFile.canWrite()) {
_queriedPropsOutFile = null;
LOG.warn("Unable to write to ''{0}'' file, the test parameters will not be stored", pOut);
} else {
LOG.info("Storing parameter values to ''{0}'', you can rerun the test "
+ "with the same parameters later", pOut);
}
} catch (IOException ioe) {
LOG.warn(ioe, "Unable to create ''{0}'' file, the test parameters will not be stored", pOut);
}
}
this.dumpBuffer = new StringBuilder();
this.dumpBufferNotFound = new StringBuilder();
this.dumpBufferDefaultVal = new StringBuilder();
}
private void initSnapshot() {
// get snapshot output file, if provided
String pOut = System.getProperty(PARAM_PROPERTY_OUT_FILE);
if (StringUtil.isNotBlank(pOut)) {
try {
_propertyOutFile = new File(pOut);
if (!_propertyOutFile.exists()) {
_propertyOutFile.createNewFile();
}
if (!_propertyOutFile.canWrite()) {
_propertyOutFile = null;
LOG.warn("Unable to write to ''{0}'' file, the test parameters will not be stored", pOut);
} else {
LOG.info("Storing parameter values to ''{0}'', you can rerun the test "
+ "with the same parameters later", pOut);
}
} catch (IOException ioe) {
LOG.warn(ioe, "Unable to create ''{0}'' file, the test parameters will not be stored", pOut);
}
}
}
/**
* Constructor for JUnit Testing purposes only. Do not use it normally.
*/
GroovyDataProvider(URL configURL) {
configObject = doBootstrap();
// parse the configuration file once
ConfigObject highPriorityCO = cs.parse(configURL);
configObject = mergeConfigObjects(configObject, highPriorityCO);
}
/** load the bootstrap configuration */
private ConfigObject doBootstrap() {
URL url = getClass().getClassLoader().getResource(BOOTSTRAP_FILE_NAME);
String msg = String.format("Missing bootstrap file: %s. (Hint: copy "
+ "framework/test-contract/src/bootstrap.groovy to folder framework/test-contract/build)",
BOOTSTRAP_FILE_NAME);
assertNotNull(url, msg);
return cs.parse(url);
}
/**
* merge two config objects. If both of config objects contian the same
* property key, then the value of highPriorityCO
is propagated
* to the result.
*
* @param lowPriorityCO
* @param highPriorityCO
* @return the merged version of two config objects.
*/
static ConfigObject mergeConfigObjects(ConfigObject lowPriorityCO,
ConfigObject highPriorityCO) {
return (ConfigObject) lowPriorityCO.merge(highPriorityCO);
}
/**
* Main get method. Property lookup starts here.
*/
public Object get(String name, String type, boolean useDefault) {
Object o = null;
/** indicates if default value used */
boolean isDefaultValue = false;
/** indicates if the property was properly found */
boolean isFound = true;
try {
o = propertyRecursiveGet(name);
} catch (ObjectNotFoundException onfe) {
// What to do in case of missing property value:
if (useDefault) {
isDefaultValue = true;
// generate a default value
o = propertyRecursiveGet(type);
} else {
isFound = false;
if (useDefault) {
throw new ObjectNotFoundException("Missing property definition: " + name
+ ", data type: " + type);
} else {
throw new ObjectNotFoundException("Missing property definition: " + name);
}
}
} finally {
if (_queriedPropsOutFile != null) {
logQueriedProperties(o, name, type, isDefaultValue, isFound);
}
}
// resolve o.n.f.e.
if (o instanceof ObjectNotFoundException) {
throw (ObjectNotFoundException) o;
}
// cache resolved value
cache.put(name, o);
return o;
}
/**
* dump the current property query results into local buffer
*
* @param queriedObject the object value that was returned from query
* @param name the name of property that was queried
* @param type
* @param isDefaultValue if default value was returned
* @param isFound if it was succesfully found
*/
private void logQueriedProperties(Object queriedObject, String name, String type,
boolean isDefaultValue, boolean isFound) {
String msg = "name: '%s' type: '%s' defaultReturned: '%s' %s: '%s'";
String appendInfo = String.format(msg, name, type,
Boolean.toString(isDefaultValue), FOUND_MSG, Boolean.toString(isFound))
+ ((queriedObject != null) ? (" value: " + flatten(queriedObject)) : "") + "\n";
this.dumpBuffer.append(appendInfo);
if (isFound == false) {
this.dumpBufferNotFound.append(appendInfo);
}
if (isDefaultValue == true) {
this.dumpBufferDefaultVal.append(appendInfo);
}
}
/**
* try to resolve the property's value
*
* @param name
* @return
*/
private Object propertyRecursiveGet(String name) {
Object response = null;
if (!cache.containsKey(name)) {
try {
// get the property for given name
// (in case property is not found, ObjectNotFoundException will be
// thrown.)
response = configObjectRecursiveGet(name, this.configObject);
} catch (ObjectNotFoundException onfe) {
// we did not found the property for given name, try to search it
// recursively
// by deleting the first prefix
int separatorIndex = name.indexOf(PROPERTY_SEPARATOR, 0);
if (separatorIndex != SINGLE_VALUE_MARKER) {
separatorIndex++;
if (separatorIndex < name.length()) {
return propertyRecursiveGet(name.substring(separatorIndex));
}
} else {
throw new ObjectNotFoundException(
"Can't find object for key: " + name);
}// fi
}// catch
} else {
response = cache.get(name);
}
return response;
}
/**
* Contains key functionality for acquiring properties with hierarchical
* names (e.g. foo.bar.spam) from ConfigObject.
*
* @param name property name
* @param co configuration model, that contains all the property key/value pairs
* @return
* @throws ObjectNotFoundException
*/
private Object configObjectRecursiveGet(String name, ConfigObject co) {
int dotIndex = name.indexOf(PROPERTY_SEPARATOR);
if (dotIndex >= 0) {
String currentNamePart = name.substring(0, dotIndex);
/*
* request the property name from parsed config file
*/
Object o = configObjectGet(co, currentNamePart);
if (o instanceof ConfigObject) {
// recursively resolve the hierarchical names (containing
// multiple dots.
return configObjectRecursiveGet(name.substring(dotIndex + 1), (ConfigObject) o);
} else {
final String MSG = "Unexpected object instance. "
+ "Searching property: '%s', found value: '%s', expected value is ConfigObject."
+ "Please check that property '%s' is defined - "
+ "it can collide with attribute value definition.";
fail(String.format(MSG, name, o.toString(), name));
return null;
}// fi inner
} else {
/*
* request the property name from parsed config file
*/
return configObjectGet(co, name);
}
}
/**
*
* @param co current config object which is queried
* @param currentNamePart the queried property name
* @return the value for given property name
* @throws ObjectNotFoundException
*/
private Object configObjectGet(ConfigObject co, String currentNamePart) {
/*
* get the property value
*/
Object result = co.getProperty(currentNamePart);
if (result instanceof ConfigObject) {
// try if property value is empty
ConfigObject coResult = (ConfigObject) result;
if (coResult.isEmpty()) {
throw new ObjectNotFoundException();
}
} else {
result = resolvePropObject(result);
}// fi
return result;
}
/**
* Resolve the special types of property object (right side of assigment operator).
* There are two types supported:
*
* - {@link List}
* - {@link Lazy}
*
*
* @param o
* @return the resolved property object
*/
private Object resolvePropObject(Object o) {
Object resolved = o;
if (o instanceof Lazy) {
Lazy lazy = (Lazy) o;
resolved = resolveLazy(lazy);
} else if (o instanceof List>) {
@SuppressWarnings("unchecked")
List