/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2011-2014 ForgeRock AS. All rights reserved.
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Portions Copyrighted 2011 Viliam Repan (lazyman)
* Portions Copyrighted 2011 Radovan Semancik
* Portions Copyrighted 2011 - 2014 Evolveum
*
*/
package com.evolveum.polygon.csvfile;
import static com.evolveum.polygon.csvfile.util.Utils.*;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.channels.FileLock;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.identityconnectors.common.StringUtil;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.AlreadyExistsException;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.ConnectionBrokenException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.exceptions.ConnectorIOException;
import org.identityconnectors.framework.common.exceptions.InvalidCredentialException;
import org.identityconnectors.framework.common.exceptions.InvalidPasswordException;
import org.identityconnectors.framework.common.exceptions.UnknownUidException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeInfo;
import org.identityconnectors.framework.common.objects.AttributeInfoBuilder;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder;
import org.identityconnectors.framework.common.objects.Name;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.ObjectClassInfoBuilder;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationalAttributeInfos;
import org.identityconnectors.framework.common.objects.OperationalAttributes;
import org.identityconnectors.framework.common.objects.ResultsHandler;
import org.identityconnectors.framework.common.objects.Schema;
import org.identityconnectors.framework.common.objects.SchemaBuilder;
import org.identityconnectors.framework.common.objects.ScriptContext;
import org.identityconnectors.framework.common.objects.SyncDelta;
import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
import org.identityconnectors.framework.common.objects.SyncDeltaType;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.common.objects.filter.AbstractFilterTranslator;
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.AuthenticateOp;
import org.identityconnectors.framework.spi.operations.CreateOp;
import org.identityconnectors.framework.spi.operations.DeleteOp;
import org.identityconnectors.framework.spi.operations.ResolveUsernameOp;
import org.identityconnectors.framework.spi.operations.SchemaOp;
import org.identityconnectors.framework.spi.operations.ScriptOnConnectorOp;
import org.identityconnectors.framework.spi.operations.ScriptOnResourceOp;
import org.identityconnectors.framework.spi.operations.SearchOp;
import org.identityconnectors.framework.spi.operations.SyncOp;
import org.identityconnectors.framework.spi.operations.TestOp;
import org.identityconnectors.framework.spi.operations.UpdateAttributeValuesOp;
import com.evolveum.polygon.csvfile.sync.Change;
import com.evolveum.polygon.csvfile.sync.InMemoryDiff;
import com.evolveum.polygon.csvfile.util.CSVSchemaException;
import com.evolveum.polygon.csvfile.util.CsvItem;
import com.evolveum.polygon.csvfile.util.TokenFileNameFilter;
import com.evolveum.polygon.csvfile.util.Utils;
import java.io.*;
import java.nio.channels.FileLock;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Pattern;
/**
* Main implementation of the CSVFile Connector
*
* @author Viliam Repan (lazyman)
* @author $author$
* @version $Revision$ $Date$
*/
@ConnectorClass(displayNameKey = "UI_CONNECTOR_NAME",
configurationClass = CSVFileConfiguration.class)
public class CSVFileConnector implements Connector, AuthenticateOp, ResolveUsernameOp, CreateOp, DeleteOp, SchemaOp,
SearchOp<String>, SyncOp, TestOp, UpdateAttributeValuesOp, ScriptOnResourceOp, ScriptOnConnectorOp {
/**
* Setup logging for the {@link CSVFileConnector}.
*/
private static final Log log = Log.getLog(CSVFileConnector.class);
public static final String TMP_EXTENSION = ".tmp";
private static enum Operation {
DELETE, UPDATE, ADD_ATTR_VALUE, REMOVE_ATTR_VALUE;
}
private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
private static final DateFormat FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z");
private Pattern linePattern;
/**
* Place holder for the {@link org.identityconnectors.framework.spi.Configuration} passed into the init() method
* {@link CSVFileConnector#init(org.identityconnectors.framework.spi.Configuration)}.
*/
private CSVFileConfiguration configuration;
/**
* Gets the Configuration context for this connector.
*/
public Configuration getConfiguration() {
return this.configuration;
}
/**
* Callback method to receive the {@link org.identityconnectors.framework.spi.Configuration}.
*
* @see org.identityconnectors.framework.spi.Connector#init(org.identityconnectors.framework.spi.Configuration)
*/
public void init(Configuration initialConfiguration1) {
notNullArgument(initialConfiguration1, "configuration");
this.configuration = (CSVFileConfiguration) initialConfiguration1;
String fieldDelimiter = Pattern.quote(configuration.getFieldDelimiter());
// regexp with ," chars is (?:^|,)(\"(?:[^\"]+|\"\")*\"|[^,]*)
StringBuilder builder = new StringBuilder();
builder.append("(?:^|");
builder.append(fieldDelimiter);
builder.append(")(");
builder.append(this.configuration.getValueQualifier());
builder.append("(?:[^");
builder.append(this.configuration.getValueQualifier());
builder.append("]+|");
builder.append(this.configuration.getValueQualifier());
builder.append(this.configuration.getValueQualifier());
builder.append(")*");
builder.append(this.configuration.getValueQualifier());
builder.append("|[^");
builder.append(fieldDelimiter);
builder.append("]*)");
linePattern = Pattern.compile(builder.toString());
}
/**
* Disposes of the {@link CSVFileConnector}'s resources.
*
* @see org.identityconnectors.framework.spi.Connector#dispose()
*/
public void dispose() {
}
/**
* ****************
* SPI Operations
*
* Implement the following operations using the contract and description
* found in the Javadoc for these methods.
*****************
*/
/**
* {@inheritDoc}
*/
public Uid authenticate(final ObjectClass objectClass, final String userName, final GuardedString password, final OperationOptions options) {
log.ok("authenticate::begin");
Uid uid = realAuthenticate(objectClass, userName, password, true);
log.ok("authenticate::end");
return uid;
}
/**
* {@inheritDoc}
*/
public Uid resolveUsername(final ObjectClass objectClass, final String userName, final OperationOptions options) {
log.ok("resolveUsername::begin");
Uid uid = realAuthenticate(objectClass, userName, null, false);
log.ok("resolveUsername::end");
return uid;
}
/**
* {@inheritDoc}
*/
public Uid create(final ObjectClass objectClass, final Set<Attribute> createAttributes, final OperationOptions options) {
log.ok("create::begin");
isAccount(objectClass);
notNull(createAttributes, "Attribute set must not be null.");
Attribute uidAttr = getAttribute(configuration.getUniqueAttribute(), createAttributes);
if (uidAttr == null || uidAttr.getValue().isEmpty() || uidAttr.getValue().get(0) == null) {
throw new UnknownUidException("Unique attribute not defined or is empty.");
}
Uid uid = new Uid(uidAttr.getValue().get(0).toString());
BufferedReader reader;
BufferedWriter writer = null;
LOCK.writeLock().lock();
try {
reader = createReader(configuration);
List<String> header = readHeader(reader, linePattern, configuration);
CsvItem account = findAccount(reader, header, uid.getUidValue());
closeReader(reader, null);
if (account != null) {
throw new AlreadyExistsException("Account already exists '" + uid.getUidValue() + "'.");
}
StringBuilder record = createRecord(header, createAttributes);
if (record.length() == 0) {
throw new ConnectorException("Can't insert empty record.");
}
FileInputStream fis = new FileInputStream(configuration.getFilePath());
fis.skip(configuration.getFilePath().length() - 1);
byte[] chars = new byte[1];
fis.read(chars);
fis.close();
writer = createWriter(true);
if (chars[0] != 10) { // 10 is the decimal value for \n
writer.write('\n');
}
writer.append(record);
writer.append('\n');
} catch (Exception ex) {
handleGenericException(ex, "Couldn't create account");
} finally {
closeWriter(writer, null);
LOCK.writeLock().unlock();
}
log.ok("create::end");
return uid;
}
/**
* {@inheritDoc}
*/
public void delete(final ObjectClass objectClass, final Uid uid, final OperationOptions options) {
log.ok("delete::begin");
doUpdate(Operation.DELETE, objectClass, uid, null, options);
log.ok("delete::end");
}
/**
* {@inheritDoc}
*/
public Schema schema() {
log.ok("schema::begin");
List<String> headers = null;
BufferedReader reader = null;
LOCK.readLock().lock();
try {
reader = createReader(configuration);
headers = readHeader(reader, linePattern, configuration);
testHeader(headers);
} catch (Exception ex) {
handleGenericException(ex, "Couldn't create schema");
} finally {
closeReader(reader, null);
LOCK.readLock().unlock();
}
if (headers == null || headers.isEmpty()) {
throw new CSVSchemaException("Schema can't be generated, header is null (probably not defined in file - first line in csv).");
}
ObjectClassInfoBuilder objClassBuilder = new ObjectClassInfoBuilder();
objClassBuilder.addAllAttributeInfo(createAttributeInfo(headers));
SchemaBuilder builder = new SchemaBuilder(CSVFileConnector.class);
builder.defineObjectClass(objClassBuilder.build());
log.ok("schema::end");
return builder.build();
}
/**
* {@inheritDoc}
*/
public Object runScriptOnConnector(ScriptContext request, OperationOptions options) {
// Connector and resource are the same in this case
return runScriptOnResource(request, options);
}
/**
* {@inheritDoc}
*/
public Object runScriptOnResource(ScriptContext request, OperationOptions options) {
String command = request.getScriptText();
String[] commandList = command.split("\\s+");
ProcessBuilder pb = new ProcessBuilder(commandList);
Map<String, String> env = pb.environment();
for (Entry<String,Object> argEntry: request.getScriptArguments().entrySet()) {
String varName = argEntry.getKey();
Object varValue = argEntry.getValue();
if (varValue == null) {
env.remove(varName);
} else {
env.put(varName, varValue.toString());
}
}
Process process;
try {
log.ok("Executing ''{0}''", command);
process = pb.start();
} catch (IOException e) {
log.error("Execution of ''{0}'' failed (exec): {1} ({2})", command, e.getMessage(), e.getClass());
throw new ConnectorIOException(e.getMessage(), e);
}
try {
int exitCode = process.waitFor();
log.ok("Execution of ''{0}'' finished, exit code {1}", command, exitCode);
return exitCode;
} catch (InterruptedException e) {
log.error("Execution of ''{0}'' failed (waitFor): {1} ({2})", command, e.getMessage(), e.getClass());
throw new ConnectionBrokenException(e.getMessage(), e);
}
}
/**
* {@inheritDoc}
*/
public FilterTranslator<String> createFilterTranslator(ObjectClass objectClass, OperationOptions options) {
log.ok("createFilterTranslator::begin");
isAccount(objectClass);
log.ok("createFilterTranslator::end");
return new AbstractFilterTranslator<String>() {
};
}
/**
* {@inheritDoc}
*/
public void executeQuery(ObjectClass objectClass, String query, ResultsHandler handler, OperationOptions options) {
log.ok("executeQuery::begin");
isAccount(objectClass);
notNull(handler, "Results handled object can't be null.");
BufferedReader reader = null;
LOCK.readLock().lock();
try {
reader = createReader(configuration);
List<String> header = readHeader(reader, linePattern, configuration);
String line;
CsvItem item;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (isEmptyOrComment(line)) {
continue;
}
item = Utils.createCsvItem(header, line, lineNumber, linePattern, configuration);
ConnectorObject object = createConnectorObject(header, item);
if (!handler.handle(object)) {
break;
}
}
} catch (Exception ex) {
handleGenericException(ex, "Can't execute query");
} finally {
closeReader(reader, null);
LOCK.readLock().unlock();
}
log.ok("executeQuery::end");
}
/**
* method use when there is not old sync files - no sync token available. New sync file with token (time) is
* created. It means we're synchronizing from now on.
*
* @return new token value
*/
private String createNewSyncFile() {
long timestamp = configuration.getFilePath().lastModified();
File syncFile = new File(configuration.getFilePath().getParentFile(),
configuration.getFilePath().getName() + "." + timestamp);
LOCK.writeLock().lock();
try {
copyAndReplace(configuration.getFilePath(), syncFile);
} catch (Exception ex) {
handleGenericException(ex, "Couldn't create file copy for sync");
} finally {
LOCK.writeLock().unlock();
}
return Long.toString(timestamp);
}
/**
* {@inheritDoc}
*/
public void sync(ObjectClass objectClass, SyncToken token, SyncResultsHandler handler, final OperationOptions options) {
log.ok("sync::begin");
isAccount(objectClass);
notNull(handler, "Sync results handler must not be null.");
long tokenLongValue = getTokenValue(token);
log.info("Token {0}", tokenLongValue);
if (tokenLongValue == -1) {
//token doesn't exist, we only create new sync file - we're synchronizing from now on
createNewSyncFile();
log.info("Token value was not defined {0}, only creating new sync file, synchronizing from now on.", token);
log.ok("sync::end");
return;
}
boolean hasFileChanged = false;
long lastModified = configuration.getFilePath().lastModified();
if (lastModified == 0L) {
// very suspicious, so let's check it
if (!configuration.getFilePath().exists()) {
throw new ConnectorException("File " + configuration.getFilePath() + " does not exist or is not accessible");
}
}
if (lastModified > tokenLongValue) {
hasFileChanged = true;
log.info("Csv file has changed on {0} which is after time {1}, based on token value {2}",
FORMAT.format(new Date(lastModified)),
FORMAT.format(new Date(tokenLongValue)), tokenLongValue);
}
if (!hasFileChanged) {
log.info("File has not changed after {0} (token value {1}), diff will be skipped.",
FORMAT.format(new Date(tokenLongValue)), tokenLongValue);
log.ok("sync::end");
return;
}
syncReal(tokenLongValue, handler);
log.ok("sync::end");
}
private void syncReal(long tokenLongValue, SyncResultsHandler handler) {
long timestamp = configuration.getFilePath().lastModified();
log.ok("Next last sync token value will be {0} ({1}).", timestamp, FORMAT.format(new Date(timestamp)));
File syncFile = new File(configuration.getFilePath().getParentFile(),
configuration.getFilePath().getName() + "." + timestamp + TMP_EXTENSION);
LOCK.writeLock().lock();
try {
copyAndReplace(configuration.getFilePath(), syncFile);
} catch (Exception ex) {
handleGenericException(ex, "Could not create file copy for sync");
} finally {
LOCK.writeLock().unlock();
}
File tokenSyncFile = new File(configuration.getFilePath().getParent(), configuration.getFilePath().getName()
+ "." + tokenLongValue);
log.info("Diff actual file {0} with last file based on token {1}.", syncFile.getName(), tokenSyncFile.getName());
InMemoryDiff memoryDiff = new InMemoryDiff(tokenSyncFile, syncFile, linePattern, configuration);
try {
List<Change> changes = memoryDiff.diff();
log.info("Found {0} differences.", changes.size());
if (changes.size() == 0) {
//this was only phantom change, nothing was really changed, delete sync file (new token not necessary)
log.info("Deleting file {0}.", syncFile.getName());
syncFile.delete();
return;
}
SyncToken newToken = new SyncToken(Long.toString(timestamp));
for (Change change : changes) {
SyncDelta delta = createSyncDelta(change, newToken);
if (!handler.handle(delta)) {
break;
}
}
File newFile = new File(configuration.getFilePath().getParent(), configuration.getFilePath().getName() + "." + timestamp);
log.info("Renaming file {0} to {1}.", syncFile.getName(), newFile.getName());
syncFile.renameTo(newFile);
cleanupOldTokenFiles();
} catch (Exception ex) {
handleGenericException(ex, "Couldn't finish sync operation");
}
}
private void handleGenericException(Exception ex, String message) {
log.ok(ex, "Exception occurred: {0} , reason {1}", message, ex.getMessage());
if (ex instanceof ConnectorException) {
throw (ConnectorException) ex;
}
if (ex instanceof IOException) {
throw new ConnectorIOException(message + ", IO exception occurred, reason: " + ex.getMessage(), ex);
}
throw new ConnectorException(message + ", reason: " + ex.getMessage(), ex);
}
private void cleanupOldTokenFiles() {
String[] tokenFiles = listTokenFiles();
Arrays.sort(tokenFiles);
int preserve = configuration.getPreserveLastTokens();
if (preserve <= 1) {
log.info("Not removing old token files. Preserve last tokens: {0}.", preserve);
return;
}
File parentFolder = configuration.getFilePath().getParentFile();
for (int i = 0; i + preserve < tokenFiles.length; i++) {
File tokenSyncFile = new File(parentFolder, tokenFiles[i]);
if (!tokenSyncFile.exists()) {
continue;
}
log.info("Deleting file {0}.", tokenSyncFile.getName());
tokenSyncFile.delete();
}
}
private String[] listTokenFiles() {
if (!configuration.getFilePath().exists()) {
throw new ConnectorIOException("Csv file '" + configuration.getFilePath() + "' not found.");
}
File parentFolder = configuration.getFilePath().getParentFile();
if (!parentFolder.exists() || !parentFolder.isDirectory()) {
throw new ConnectorIOException("Parent folder for '" + configuration.getFilePath()
+ "' doesn't exist, or is not a directory.");
}
String csvFileName = configuration.getFilePath().getName();
return parentFolder.list(new TokenFileNameFilter(csvFileName));
}
/**
* {@inheritDoc}
*/
public SyncToken getLatestSyncToken(ObjectClass objectClass) {
log.ok("getLatestSyncToken::begin");
isAccount(objectClass);
String csvFileName = configuration.getFilePath().getName();
String[] oldCsvFiles = listTokenFiles();
String token;
if (oldCsvFiles.length != 0) {
Arrays.sort(oldCsvFiles);
String latestCsvFile = oldCsvFiles[oldCsvFiles.length - 1];
token = latestCsvFile.replaceFirst(csvFileName + ".", "");
} else {
log.info("Old csv files were not found, creating token, synchronizing from \"now\".");
token = createNewSyncFile();
}
log.ok("getLatestSyncToken::end, returning token {0}.", token);
return new SyncToken(token);
}
/**
* {@inheritDoc}
*/
public void test() {
log.ok("test::begin");
log.info("Validating configuration.");
configuration.validate();
BufferedReader reader = null;
LOCK.readLock().lock();
try {
log.info("Opening input stream to file {0}.", configuration.getFilePath());
reader = createReader(configuration);
List<String> headers = readHeader(reader, linePattern, configuration);
testHeader(headers);
} catch (Exception ex) {
log.error("Test configuration was unsuccessful, reason: {0}.", ex.getMessage());
handleGenericException(ex, "Test configuration was unsuccessful");
} finally {
log.info("Closing file input stream.");
closeReader(reader, null);
LOCK.readLock().unlock();
}
log.info("Test configuration was successful.");
log.ok("test::end");
}
/**
* {@inheritDoc}
*/
public Uid update(ObjectClass objectClass,
Uid uid,
Set<Attribute> replaceAttributes,
OperationOptions options) {
log.ok("update::begin");
uid = doUpdate(Operation.UPDATE, objectClass, uid, replaceAttributes, options);
log.ok("update::end");
return uid;
}
/**
* {@inheritDoc}
*/
public Uid addAttributeValues(ObjectClass objectClass,
Uid uid,
Set<Attribute> valuesToAdd,
OperationOptions options) {
log.ok("addAttributeValues::begin");
uid = doUpdate(Operation.ADD_ATTR_VALUE, objectClass, uid, valuesToAdd, options);
log.ok("addAttributeValues::end");
return uid;
}
/**
* {@inheritDoc}
*/
public Uid removeAttributeValues(ObjectClass objectClass,
Uid uid,
Set<Attribute> valuesToRemove,
OperationOptions options) {
log.ok("removeAttributeValues::begin");
uid = doUpdate(Operation.REMOVE_ATTR_VALUE, objectClass, uid, valuesToRemove, options);
log.ok("removeAttributeValues::end");
return uid;
}
///////////////////////////////////////////////////////////////////////////
//
// Private Implementation
//
///////////////////////////////////////////////////////////////////////////
private BufferedWriter createWriter(boolean append) throws IOException {
return createWriter(configuration.getFilePath(), append);
}
private BufferedWriter createWriter(File path, boolean append) throws IOException {
log.ok("Creating writer.");
FileOutputStream fos = new FileOutputStream(path, append);
OutputStreamWriter out = new OutputStreamWriter(fos, configuration.getEncoding());
return new BufferedWriter(out);
}
private void closeWriter(Writer writer, FileLock lock) {
try {
if (writer != null) {
writer.flush();
writer.close();
}
unlock(lock);
} catch (IOException ex) {
throw new ConnectorException("Couldn't close writer, reason: " + ex.getMessage(), ex);
}
}
private File createTempFile() {
File file;
try {
file = new File(configuration.getFilePath().getCanonicalPath() + TMP_EXTENSION);
if (file.exists()) {
if (!file.delete()) {
throw new ConnectorIOException("Couldn't delete old tmp file '" + file.getAbsolutePath() + "'.");
}
}
file.createNewFile();
} catch (IOException ex) {
file = new File(configuration.getFilePath() + TMP_EXTENSION);
throw new ConnectorIOException("Couldn't create tmp file '" + file.getAbsolutePath()
+ "', reason: " + ex.getMessage(), ex);
}
return file;
}
private CsvItem findAccount(BufferedReader reader, List<String> header, String username) throws IOException {
int lineNumber = 1;
String line;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (isEmptyOrComment(line)) {
continue;
}
CsvItem item = Utils.createCsvItem(header, line, lineNumber, linePattern, configuration);
int fieldIndex = header.indexOf(configuration.getUniqueAttribute());
String value = item.getAttribute(fieldIndex);
if (!StringUtil.isEmpty(value) && value.equals(username)) {
return item;
}
}
return null;
}
private StringBuilder createRecord(List<String> header, Set<Attribute> attributes) {
final StringBuilder builder = new StringBuilder();
for (String name : header) {
if (header.indexOf(name) != 0) {
builder.append(configuration.getFieldDelimiter());
}
Attribute attribute = getAttribute(name, attributes);
if (configuration.getUniqueAttribute().equals(name)) {
if (attribute == null || attribute.getValue().isEmpty()) {
throw new CSVSchemaException("Unique attribute for record is not defined.");
}
}
if (attribute == null) {
continue;
}
String value = appendValues(name, attribute.getValue());
if (StringUtil.isNotEmpty(value)) {
appendQualifiedValue(builder, value);
}
}
return builder;
}
private Attribute getAttribute(String name, Set<Attribute> attributes) {
if (name.equals(configuration.getPasswordAttribute())) {
name = OperationalAttributes.PASSWORD_NAME;
}
if (name.equals(configuration.getNameAttribute())) {
name = Name.NAME;
}
for (Attribute attribute : attributes) {
if (attribute.getName().equals(name)) {
return attribute;
}
}
return null;
}
private List<AttributeInfo> createAttributeInfo(List<String> names) {
List<AttributeInfo> infos = new ArrayList<AttributeInfo>();
for (String name : names) {
if (name.equals(configuration.getUniqueAttribute())) {
continue;
}
if (name.equals(configuration.getNameAttribute())) {
continue;
}
if (name.equals(configuration.getPasswordAttribute())) {
infos.add(OperationalAttributeInfos.PASSWORD);
continue;
}
AttributeInfoBuilder builder = new AttributeInfoBuilder(name);
if (name.equals(configuration.getPasswordAttribute())) {
builder.setType(GuardedString.class);
} else {
builder.setType(String.class);
}
infos.add(builder.build());
}
return infos;
}
private ConnectorObject createConnectorObject(List<String> header, CsvItem item) {
ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
for (int i = 0; i < header.size(); i++) {
if (StringUtil.isEmpty(item.getAttribute(i))) {
continue;
}
String name = header.get(i);
if (name.equals(configuration.getUniqueAttribute())) {
builder.setUid(item.getAttribute(i));
// builder.addAttribute(name, item.getAttribute(i));
if (!configuration.isUniqueAndNameAttributeEqual()) {
continue;
}
}
if (name.equals(configuration.getNameAttribute())) {
builder.setName(new Name(item.getAttribute(i)));
continue;
}
if (name.equals(configuration.getPasswordAttribute())) {
builder.addAttribute(OperationalAttributes.PASSWORD_NAME, new GuardedString(item.getAttribute(i).toCharArray()));
continue;
}
builder.addAttribute(name, createAttributeValues(item.getAttribute(i)));
}
return builder.build();
}
private List<String> createAttributeValues(String attributeValue) {
List<String> values = new ArrayList<String>();
if (!configuration.isUsingMultivalue()) {
values.add(attributeValue);
return values;
}
String[] array = attributeValue.split(Pattern.quote(configuration.getMultivalueDelimiter()));
for (String val : array) {
if (val != null) {
values.add(val);
}
}
return values;
}
private Uid realAuthenticate(ObjectClass objectClass, String username, GuardedString pwd, boolean testPassword) {
log.ok("realAuthenticate::begin");
isAccount(objectClass);
if (username == null) {
throw new InvalidCredentialException("Username can't be null.");
}
if (testPassword && StringUtil.isEmpty(configuration.getPasswordAttribute())) {
throw new ConfigurationException("Password attribute not defined in configuration.");
}
if (testPassword && pwd == null) {
throw new InvalidPasswordException("Password can't be null.");
}
BufferedReader reader = null;
LOCK.readLock().lock();
try {
reader = createReader(configuration);
List<String> header = readHeader(reader, linePattern, configuration);
CsvItem account = findAccount(reader, header, username);
if (account == null) {
String message;
if (testPassword) {
message = "Invalid username and/or password.";
} else {
message = "Invalid username.";
}
throw new InvalidCredentialException(message);
}
int index;
if (testPassword) {
index = header.indexOf(configuration.getPasswordAttribute());
final String password = account.getAttribute(index);
if (StringUtil.isEmpty(password)) {
throw new InvalidPasswordException("Invalid username and/or password.");
}
pwd.access(new GuardedString.Accessor() {
public void access(char[] chars) {
if (!new String(chars).equals(password)) {
throw new InvalidPasswordException("Invalid username and/or password.");
}
}
});
}
index = header.indexOf(configuration.getUniqueAttribute());
String uidAttribute = account.getAttribute(index);
if (StringUtil.isEmpty(uidAttribute)) {
throw new UnknownUidException("Unique atribute doesn't have value for account '" + username + "'.");
}
log.ok("realAuthenticate::end");
return new Uid(uidAttribute);
} catch (Exception ex) {
handleGenericException(ex, "Can't authenticate '" + username + "'");
//it won't go here
return null;
} finally {
closeReader(reader, null);
log.info("realAuthenticate::end");
LOCK.readLock().unlock();
}
}
private Uid doUpdate(Operation operation, ObjectClass objectClass, Uid uid, Set<Attribute> attributes, OperationOptions oo) {
log.ok("doUpdate::begin");
isAccount(objectClass);
notNull(uid, "Uid must not be null.");
if (attributes == null && Operation.DELETE != operation) {
throw new IllegalArgumentException("Attribute set can't be null.");
}
BufferedReader reader = null;
BufferedWriter writer = null;
LOCK.writeLock().lock();
File tmpFile = createTempFile();
try {
reader = createReader(configuration);
writer = createWriter(tmpFile, true);
List<String> header = readHeader(reader, writer, linePattern, configuration);
ConnectorObject changed = readAndUpdateFile(reader, writer, header, operation, uid, attributes);
if (changed == null) {
throw new UnknownUidException("Uid '" + uid.getUidValue() + "' not found in file.");
}
uid = changed.getUid();
closeReader(reader, null);
closeWriter(writer, null);
reader = null;
writer = null;
if (configuration.getFilePath().delete()) {
tmpFile.renameTo(configuration.getFilePath());
} else {
throw new ConnectorIOException("Couldn't delete old file '" + configuration.getFilePath().getAbsolutePath()
+ "' and replace it by new file '" + tmpFile.getAbsolutePath() + "'.");
}
} catch (Exception ex) {
handleGenericException(ex, "Couldn't do " + operation + " on account '" + uid.getUidValue() + "'");
} finally {
closeReader(reader, null);
closeWriter(writer, null);
try {
if (tmpFile.exists()) {
tmpFile.delete();
}
} catch (Exception ex) {
//only try to cleanup tmp file, it will be replaced later, if exists
}
LOCK.writeLock().unlock();
}
log.ok("doUpdate::end");
return uid;
}
private ConnectorObject readAndUpdateFile(BufferedReader reader, BufferedWriter writer, List<String> header,
Operation operation, Uid uid, Set<Attribute> attributes) throws IOException {
ConnectorObject changed = null;
String line;
int lineNumber = 1;
CsvItem item;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (isEmptyOrComment(line)) {
writer.write(line);
writer.write('\n');
continue;
}
item = Utils.createCsvItem(header, line, lineNumber, linePattern, configuration);
int fieldIndex = header.indexOf(configuration.getUniqueAttribute());
String value = item.getAttribute(fieldIndex);
if (!StringUtil.isEmpty(value) && value.equals(uid.getUidValue())) {
switch (operation) {
case UPDATE:
case ADD_ATTR_VALUE:
case REMOVE_ATTR_VALUE:
line = updateLine(operation, header, item, attributes);
changed = createConnectorObject(header, Utils.createCsvItem(header, line, lineNumber,
linePattern, configuration));
break;
case DELETE:
changed = createConnectorObject(header, item);
continue;
}
}
writer.write(line);
writer.write('\n');
}
return changed;
}
private String appendValues(String attributeName, List<Object> values) {
if (configuration.getUniqueAttribute().equals(attributeName)) {
if (values.size() > 1) {
throw new CSVSchemaException("Can't store unique attribute '" + attributeName
+ "' with multiple values (" + values.size() + ").");
} else if (values == null || values.isEmpty()) {
throw new CSVSchemaException("Can't store unique attribute '" + attributeName + "' without values.");
}
}
final StringBuilder builder = new StringBuilder();
if (values == null || values.isEmpty()) {
return null;
}
for (int i = 0; i < values.size(); i++) {
Object object = values.get(i);
if (object == null) {
return null;
}
if (i != 0) {
builder.append(configuration.getMultivalueDelimiter());
}
if (object instanceof GuardedString) {
GuardedString pwd = (GuardedString) object;
pwd.access(new GuardedString.Accessor() {
public void access(char[] chars) {
builder.append(chars);
}
});
} else {
builder.append(object);
}
}
return builder.toString();
}
private String appendMergedValues(String attributeName, final List<String> oldValues,
List<Object> newValues, Operation operation) {
List<Object> values = new ArrayList<Object>();
if (!configuration.isUsingMultivalue()) {
switch (operation) {
case ADD_ATTR_VALUE:
return appendValues(attributeName, newValues);
case REMOVE_ATTR_VALUE:
values = removeOldValues(oldValues, newValues);
break;
}
} else {
if (operation == Operation.REMOVE_ATTR_VALUE && (newValues == null || newValues.isEmpty())) {
return null;
}
if (operation == Operation.REMOVE_ATTR_VALUE) {
values = removeOldValues(oldValues, newValues);
} else {
values.addAll(oldValues);
values.addAll(newValues);
}
}
String value = appendValues(attributeName, values);
if (StringUtil.isNotEmpty(value)) {
return value;
}
return "";
}
private List<Object> removeOldValues(List<String> oldValues, List<Object> newValues) {
final List<Object> values = new ArrayList<Object>();
values.addAll(oldValues);
for (Object object : newValues) {
if (object instanceof String) {
values.remove((String) object);
} else if (object instanceof GuardedString) {
GuardedString guarded = (GuardedString) object;
guarded.access(new GuardedString.Accessor() {
public void access(char[] chars) {
values.remove(new String(chars));
}
});
}
}
return values;
}
private String updateLine(Operation operation, List<String> headers, CsvItem item, Set<Attribute> attributes) {
StringBuilder builder = new StringBuilder();
String value = null;
for (String header : headers) {
int index = headers.indexOf(header);
if (index != 0) {
builder.append(configuration.getFieldDelimiter());
}
Attribute attribute = getAttribute(header, attributes);
if (attribute != null) {
switch (operation) {
case UPDATE:
value = appendValues(header, attribute.getValue());
break;
case ADD_ATTR_VALUE:
case REMOVE_ATTR_VALUE:
List<String> oldValues = new ArrayList<String>();
String oldValuesStr = item.getAttribute(index);
if (StringUtil.isNotEmpty(oldValuesStr)) {
if (configuration.isUsingMultivalue()) {
String[] array = oldValuesStr.split(String.valueOf(
configuration.getMultivalueDelimiter()));
oldValues.addAll(Arrays.asList(array));
} else {
oldValues.add(oldValuesStr);
}
}
value = appendMergedValues(header, oldValues, attribute.getValue(), operation);
break;
}
} else {
value = item.getAttribute(index);
}
if (StringUtil.isNotEmpty(value)) {
appendQualifiedValue(builder, value);
}
}
return builder.toString();
}
private void appendQualifiedValue(StringBuilder builder, String value) {
boolean useQualifier = configuration.getAlwaysQualify() || mustUseQualifier(value);
if (useQualifier) {
builder.append(configuration.getValueQualifier());
}
builder.append(value);
if (useQualifier) {
builder.append(configuration.getValueQualifier());
}
}
private boolean mustUseQualifier(String value) {
return value.contains(Pattern.quote(configuration.getFieldDelimiter()));
}
private void testHeader(List<String> headers) {
boolean uniqueFound = false;
boolean passwordFound = false;
Map<String, Integer> headerCount = new HashMap<String, Integer>();
for (String header : headers) {
if (!headerCount.containsKey(header)) {
headerCount.put(header, 0);
}
headerCount.put(header, headerCount.get(header) + 1);
}
for (String header : headers) {
int count = headerCount.containsKey(header) ? headerCount.get(header) : 0;
if (count != 1) {
throw new ConfigurationException("Column header '" + header
+ "' occurs more than once (" + count + ").");
}
if (header.equals(configuration.getUniqueAttribute())) {
uniqueFound = true;
continue;
}
if (StringUtil.isNotEmpty(configuration.getPasswordAttribute())
&& header.equals(configuration.getPasswordAttribute())) {
passwordFound = true;
}
if (uniqueFound && passwordFound) {
break;
}
}
if (!uniqueFound) {
throw new ConfigurationException("Header in csv file doesn't contain "
+ "unique attribute name as defined in configuration.");
}
if (StringUtil.isNotEmpty(configuration.getPasswordAttribute()) && !passwordFound) {
throw new ConfigurationException("Header in csv file doesn't contain "
+ "password attribute name as defined in configuration.");
}
}
private long getTokenValue(SyncToken token) {
if (token == null || token.getValue() == null) {
return -1;
}
String object = token.getValue().toString();
if (!object.matches("[0-9]{13}")) {
return -1;
}
return Long.parseLong(object);
}
private SyncDelta createSyncDelta(Change change, SyncToken token) {
SyncDeltaBuilder builder = new SyncDeltaBuilder();
builder.setUid(new Uid(change.getUid()));
builder.setToken(token);
if (Change.Type.DELETE.equals(change.getType())) {
builder.setDeltaType(SyncDeltaType.DELETE);
} else {
builder.setDeltaType(SyncDeltaType.CREATE_OR_UPDATE);
CsvItem item = new CsvItem(change.getAttributes());
ConnectorObject object = createConnectorObject(change.getHeader(), item);
builder.setObject(object);
}
return builder.build();
}
/**
* This method is only for tests!
*
* @return pattern used for matcher and line parsing
*/
Pattern getLinePattern() {
return linePattern;
}
}