Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@

import static org.identityconnectors.databasetable.DatabaseTableConstants.*;

import java.util.Arrays;
import java.util.Set;
import org.identityconnectors.common.CollectionUtil;
import org.identityconnectors.common.StringUtil;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
Expand Down Expand Up @@ -737,6 +740,68 @@ public void setLastLoginDateColumn(String lastLoginDateColumn) {
this.lastLoginDateColumn = lastLoginDateColumn;
}

/**
* Multivalue columns. Column names listed here are treated as multivalue: the single DB string
* value is parsed into multiple ICF attribute values on read. Only String columns are supported.
*/
private String[] multivalueColumns;

@ConfigurationProperty(order = 33,
displayMessageKey = "MULTIVALUE_COLUMNS_DISPLAY",
helpMessageKey = "MULTIVALUE_COLUMNS_HELP")
public String[] getMultivalueColumns() {
return multivalueColumns;
}

public void setMultivalueColumns(String[] multivalueColumns) {
this.multivalueColumns = multivalueColumns;
}

/**
* Fully-qualified class name of a custom {@code MultivalueParser} implementation.
* If blank, the default single-character split parser is used.
*/
private String multivalueParser = EMPTY_STR;

@ConfigurationProperty(order = 34,
displayMessageKey = "MULTIVALUE_PARSER_DISPLAY",
helpMessageKey = "MULTIVALUE_PARSER_HELP")
public String getMultivalueParser() {
return multivalueParser;
}

public void setMultivalueParser(String multivalueParser) {
this.multivalueParser = multivalueParser;
}

/**
* Configuration string passed to the multivalue parser.
* For the default parser this is the single separator character (default: {@code |}).
*/
private String multivalueParserConfig = EMPTY_STR;

@ConfigurationProperty(order = 35,
displayMessageKey = "MULTIVALUE_PARSER_CONFIG_DISPLAY",
helpMessageKey = "MULTIVALUE_PARSER_CONFIG_HELP")
public String getMultivalueParserConfig() {
return multivalueParserConfig;
}

public void setMultivalueParserConfig(String multivalueParserConfig) {
this.multivalueParserConfig = multivalueParserConfig;
}

/**
* Returns a case-insensitive set of multivalue column names. Returns empty set if none configured.
*/
public Set<String> getMultivalueColumnsSet() {
Set<String> result = CollectionUtil.newCaseInsensitiveSet();
if (multivalueColumns != null) {
result.addAll(Arrays.asList(multivalueColumns));
}
return result;
}

// =======================================================================
// Configuration Interface
// =======================================================================
Expand Down Expand Up @@ -835,6 +900,13 @@ private void validateConfigurationForTable() {
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(getMessage(MSG_INVALID_QUOTING, getQuoting()));
}

if (multivalueColumns != null && multivalueColumns.length > 0
&& StringUtil.isBlank(multivalueParser)
&& StringUtil.isNotBlank(multivalueParserConfig)
&& multivalueParserConfig.length() != 1) {
throw new IllegalArgumentException(getMessage(MSG_MULTIVALUE_PARSER_CONFIG_INVALID));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.databasetable.mapping.MappingStrategy;
import org.identityconnectors.databasetable.mapping.misc.SQLColumnTypeInfo;
import org.identityconnectors.databasetable.multivalue.MultivalueParser;
import org.identityconnectors.dbcommon.*;
import org.identityconnectors.dbcommon.DatabaseQueryBuilder.OrderBy;
import org.identityconnectors.framework.common.exceptions.*;
Expand Down Expand Up @@ -109,6 +110,16 @@ public class DatabaseTableConnector implements PoolableConnector, CreateOp, Sear
*/
private Set<String> stringColumnRequired;

/**
* Parser for multivalue columns; null if no multivalue columns are configured.
*/
private MultivalueParser multivalueParser;

/**
* Case-insensitive set of column names that should be treated as multivalue.
*/
private Set<String> multivalueColumnsSet;

// =======================================================================
// Initialize/dispose methods..
// =======================================================================
Expand All @@ -130,6 +141,8 @@ public void init(Configuration cfg) {
this.schema = null;
this.defaultAttributesToGet = null;
this.columnSQLTypes = null;
this.multivalueParser = null;
this.multivalueColumnsSet = null;
log.ok("init DatabaseTable connector ok, connection is valid");
}

Expand Down Expand Up @@ -182,6 +195,8 @@ public void dispose() {
this.defaultAttributesToGet = null;
this.schema = null;
this.columnSQLTypes = null;
this.multivalueParser = null;
this.multivalueColumnsSet = null;
}

/**
Expand Down Expand Up @@ -936,6 +951,16 @@ public String getColumnName(String attributeName) {
* Cache schema, defaultAtributesToGet, columnClassNamens
*/
private void cacheSchema() {
/*
* Initialize multivalue support before building attribute infos.
*/
multivalueColumnsSet = config.getMultivalueColumnsSet();
if (!multivalueColumnsSet.isEmpty()) {
multivalueParser = MultivalueParser.getInstance(config);
} else {
multivalueParser = null;
}

/*
* First, compute the account attributes based on the database schema
*/
Expand Down Expand Up @@ -1092,6 +1117,11 @@ private Set<AttributeInfo> buildAttributeInfoSet(ResultSet rset) throws SQLExcep
stringColumnRequired.add(name);
}
attrBld.setReturnedByDefault(isReturnedByDefault(dataType));
if (multivalueColumnsSet != null && multivalueColumnsSet.contains(name)) {
attrBld.setType(String.class);
attrBld.setMultiValued(true);
log.ok("column {0} marked as multivalue in schema", name);
}
attrInfo.add(attrBld.build());
log.ok("the column name {0} has data type {1}", name, dataType);
}
Expand Down Expand Up @@ -1167,7 +1197,10 @@ private ConnectorObjectBuilder buildConnectorObject(Map<String, SQLParam> column
if (param != null && param.getValue() != null) {
Object paramValue = param.getValue();

if (!(paramValue instanceof UUID)) {
if (multivalueColumnsSet != null && multivalueColumnsSet.contains(columnName)) {
Collection<String> values = multivalueParser.parse(paramValue.toString());
bld.addAttribute(AttributeBuilder.build(columnName, values));
} else if (!(paramValue instanceof UUID)) {

bld.addAttribute(AttributeBuilder.build(columnName, paramValue));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public class DatabaseTableConstants {
static final String MSG_EXP_DEFAULT = "exception.default";
static final String MSG_EXP_UNKNOWN_UID = "exception.unknown.uid";
static final String MSG_EXP_TOO_MANY_UID = "exception.more.than.one.uid";
static final String MSG_MULTIVALUE_PARSER_CONFIG_INVALID = "multivalue.parser.config.invalid";

// public static final String DEFAULT_SQLSTATE_UNIQUE_CONSTRAIN_VIOLATION = "23505";
// public static final String DEFAULT_SQLSTATE_INTEGRITY_CONSTRAIN_VIOLATION = "23000";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* ====================
* 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://IdentityConnectors.dev.java.net/legal/license.txt
* 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 identityconnectors/legal/license.txt.
* 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-2022 Evolveum
*/
package org.identityconnectors.databasetable.multivalue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.identityconnectors.databasetable.DatabaseTableConfiguration;

/**
* Default multivalue parser: splits a string on a single configurable separator character.
* The separator defaults to {@code |} if not configured.
*/
public class DefaultMultivalueParser implements MultivalueParser {

private final char separator;

public DefaultMultivalueParser(char separator) {
this.separator = separator;
}

public static MultivalueParser getInstance(DatabaseTableConfiguration cfg) {
String parserConfig = cfg.getMultivalueParserConfig();
char sep = (parserConfig != null && !parserConfig.isEmpty()) ? parserConfig.charAt(0) : '|';
return new DefaultMultivalueParser(sep);
}

@Override
public Collection<String> parse(String dbColumnValue) {
if (dbColumnValue == null) {
return Collections.emptyList();
}
List<String> result = new ArrayList<>();
int start = 0;
for (int i = 0; i < dbColumnValue.length(); i++) {
if (dbColumnValue.charAt(i) == separator) {
result.add(dbColumnValue.substring(start, i));
start = i + 1;
}
}
result.add(dbColumnValue.substring(start));
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* ====================
* 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://IdentityConnectors.dev.java.net/legal/license.txt
* 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 identityconnectors/legal/license.txt.
* 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-2022 Evolveum
*/
package org.identityconnectors.databasetable.multivalue;

import java.util.Collection;
import org.identityconnectors.databasetable.DatabaseTableConfiguration;

/**
* Parser interface for multivalue column support.
* Implementations parse a single DB column string value into multiple ICF attribute values.
*/
public interface MultivalueParser {

/**
* Factory method: called once per connector init. Returns a stateless parser.
* If {@code cfg.getMultivalueParser()} is blank, returns a {@link DefaultMultivalueParser}.
* Otherwise, reflectively invokes {@code getInstance(DatabaseTableConfiguration)} on the named class.
*/
static MultivalueParser getInstance(DatabaseTableConfiguration cfg) {
String fqcn = cfg.getMultivalueParser();
if (fqcn == null || fqcn.isBlank()) {
return DefaultMultivalueParser.getInstance(cfg);
}
try {
Class<?> cls = Class.forName(fqcn);
return (MultivalueParser) cls.getMethod("getInstance", DatabaseTableConfiguration.class)
.invoke(null, cfg);
} catch (Exception e) {
throw new IllegalArgumentException("Cannot instantiate MultivalueParser: " + fqcn, e);
}
}

/**
* Parse one DB column string value into multiple ICF attribute values.
*
* @param dbColumnValue the raw string value from the database column
* @return a collection of parsed string values
*/
Collection<String> parse(String dbColumnValue);
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,11 @@ SQL_STATE_INVALID_ATTRIBUTE_VALUE_HELP=Collection of values representing SQL sta
SQL_STATE_CONFIGURATION_EXCEPTION_DISPLAY=Configuration Exception SQL state codes
SQL_STATE_CONFIGURATION_EXCEPTION_HELP=Collection of values representing SQL state codes which can be interpreted to create an Configuration exception.
LAST_LOGIN_DATE_COLUMN_DISPLAY=Last Login Date Column
LAST_LOGIN_DATE_COLUMN_HELP=Enter the name of the column in the table that will hold the last login date values. If empty, last login date capability is disabled.
LAST_LOGIN_DATE_COLUMN_HELP=Enter the name of the column in the table that will hold the last login date values. If empty, last login date capability is disabled.
MULTIVALUE_COLUMNS_DISPLAY=Multivalue Columns
MULTIVALUE_COLUMNS_HELP=Enter the names of columns whose values should be parsed into multiple ICF attribute values on read. Only String/VARCHAR columns are supported.
MULTIVALUE_PARSER_DISPLAY=Multivalue Parser Class
MULTIVALUE_PARSER_HELP=Fully-qualified class name of a custom MultivalueParser implementation. If empty, the default single-character split parser is used.
MULTIVALUE_PARSER_CONFIG_DISPLAY=Multivalue Parser Config
MULTIVALUE_PARSER_CONFIG_HELP=Configuration string for the multivalue parser. For the default parser, this is the single separator character (default: |).
multivalue.parser.config.invalid=The multivalueParserConfig must be a single character when using the default parser.
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ protected Set<Attribute> getCreateAttributeSet(DatabaseTableConfiguration cfg) t
ret.add(AttributeBuilder.build(FIRSTNAME, randomString(r, 50)));
ret.add(AttributeBuilder.build(LASTNAME, randomString(r, 50)));
ret.add(AttributeBuilder.build(EMAIL, randomString(r, 50)));
ret.add(AttributeBuilder.build(GROUPS, randomString(r, 50)));
ret.add(AttributeBuilder.build(DEPARTMENT, randomString(r, 50)));
ret.add(AttributeBuilder.build(TITLE, randomString(r, 50)));
if(!cfg.getChangeLogColumn().equalsIgnoreCase(AGE)){
Expand Down Expand Up @@ -269,7 +270,7 @@ public void testNoZeroSQLExceptions() throws Exception {
final ExpectProxy<MappingStrategy> smse = new ExpectProxy<MappingStrategy>();
MappingStrategy sms = smse.getProxy(MappingStrategy.class);
//Schema
for (int i = 0; i < 15; i++) {
for (int i = 0; i < 16; i++) {
smse.expectAndReturn("getSQLAttributeType", String.class);
}
//Create fail
Expand All @@ -295,7 +296,7 @@ public void testNonZeroSQLExceptions() throws Exception {

final ExpectProxy<MappingStrategy> smse = new ExpectProxy<MappingStrategy>();
MappingStrategy sms = smse.getProxy(MappingStrategy.class);
for (int i = 0; i < 15; i++) {
for (int i = 0; i < 16; i++) {
smse.expectAndReturn("getSQLAttributeType", String.class);
}
smse.expectAndThrow("setSQLParam", new SQLException("test reason", "411", 411));
Expand All @@ -317,7 +318,7 @@ public void testRethrowAllSQLExceptions() throws Exception {

final ExpectProxy<MappingStrategy> smse = new ExpectProxy<MappingStrategy>();
MappingStrategy sms = smse.getProxy(MappingStrategy.class);
for (int i = 0; i < 15; i++) {
for (int i = 0; i < 16; i++) {
smse.expectAndReturn("getSQLAttributeType", String.class);
}
smse.expectAndThrow("setSQLParam", new SQLException("test reason", "0", 0));
Expand Down
Loading