/* * * Copyright (c) 2010 ForgeRock Inc. 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://www.opensource.org/licenses/cddl1.php or * OpenIDM/legal/CDDLv1.0.txt * 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 OpenIDM/legal/CDDLv1.0.txt. * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted 2010 [name of copyright owner]" * * Portions Copyrighted 2011 Viliam Repan (lazyman) * * $Id$ */ package com.evolveum.polygon.csvfile.sync; import static com.evolveum.polygon.csvfile.util.Utils.*; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.util.*; import java.util.regex.Pattern; import com.evolveum.polygon.csvfile.util.PositionedCsvItem; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.ConnectorIOException; import com.evolveum.polygon.csvfile.CSVFileConfiguration; import com.evolveum.polygon.csvfile.util.CSVSchemaException; import com.evolveum.polygon.csvfile.util.CsvItem; import com.evolveum.polygon.csvfile.util.Utils; /** * * @author Viliam Repan (lazyman) */ public class InMemoryDiff { private static final Log log = Log.getLog(InMemoryDiff.class); private CSVFileConfiguration configuration; private Pattern linePattern; private File oldFile; private File newFile = null; public InMemoryDiff(File oldFile, File newFile, Pattern linePattern, CSVFileConfiguration configuration) { notNullArgument(newFile, "newFile"); notNullArgument(linePattern, "linePattern"); notNullArgument(configuration, "configuration"); this.oldFile = oldFile; this.newFile = newFile; this.configuration = configuration; this.linePattern = linePattern; } @SuppressWarnings("unchecked") public List<Change> diff() throws DiffException { log.info("Computing diff from old {0} ({1}) and new {2} ({3}).", (oldFile != null ? oldFile.getName() : "null"), (oldFile != null ? oldFile.length() : 0), newFile.getName(), newFile.length()); List<Change> changes = new ArrayList<Change>(); try { if (oldFile != null) { testHeaders(newFile, oldFile); } RecordSet newRecordSet = createRecordSet(newFile); int uidIndex = newRecordSet.getHeaders().indexOf(configuration.getUniqueAttribute()); if (oldFile != null) { RecordSet oldRecordSet = createRecordSet(oldFile); //compare records from both record sets List<PositionedCsvItem> newList = new ArrayList<>(newRecordSet.getRecords()); List<PositionedCsvItem> oldList = new ArrayList<>(oldRecordSet.getRecords()); log.info("Record set size, new: {0}, old: {1}", newList.size(), oldList.size()); if (oldList.isEmpty()) { createOneTypeChanges(newRecordSet, uidIndex, changes, Change.Type.CREATE); } else if (newList.isEmpty()) { createOneTypeChanges(oldRecordSet, uidIndex, changes, Change.Type.DELETE); } else { changes.addAll(findChanges(uidIndex, newRecordSet.getHeaders(), newList, oldList)); } } else { //everything will be add createOneTypeChanges(newRecordSet, uidIndex, changes, Change.Type.CREATE); } } catch (IOException ex) { throw new ConnectorIOException(ex.getMessage(), ex); } catch (ConnectorException ex) { throw ex; } catch (Exception ex) { throw new DiffException("Can't create csv diff, reason: " + ex.getMessage(), ex); } Collections.sort(changes, new ChangeComparator()); return changes; } private void createOneTypeChanges(RecordSet recordSet, int uidIndex, List<Change> changes, Change.Type type) { Set<PositionedCsvItem> items = recordSet.getRecords(); Iterator<PositionedCsvItem> iterator = items.iterator(); Change change; while (iterator.hasNext()) { PositionedCsvItem item = iterator.next(); change = new Change(item.getAttribute(uidIndex), type, recordSet.getHeaders(), item.getAttributes(), item.getPosition()); changes.add(change); } } private List<Change> findChanges(int uidIndex, List<String> headers, List<PositionedCsvItem> newList, List<PositionedCsvItem> oldList) { List<Change> changes = new ArrayList<>(); Change change; int newIndex = 0, oldIndex = 0; String oldUid, newUid; outer: for (; newIndex < newList.size(); newIndex++) { PositionedCsvItem newItem = newList.get(newIndex); newUid = newItem.getAttribute(uidIndex); if (oldIndex >= oldList.size()) { break; } for (; oldIndex < oldList.size();) { PositionedCsvItem oldItem = oldList.get(oldIndex); oldUid = oldItem.getAttribute(uidIndex); int compare = String.CASE_INSENSITIVE_ORDER.compare(newUid, oldUid); if (compare < 0) { PositionedCsvItem item = newList.get(newIndex); change = new Change(item.getAttribute(uidIndex), Change.Type.CREATE, headers, item.getAttributes(), item.getPosition()); changes.add(change); break; } else if (compare > 0) { PositionedCsvItem item = oldList.get(oldIndex); change = new Change(item.getAttribute(uidIndex), Change.Type.DELETE, headers, item.getAttributes(), item.getPosition()); changes.add(change); oldIndex++; } else { if (!isEqual(newItem, oldItem)) { change = new Change(newItem.getAttribute(uidIndex), Change.Type.MODIFY, headers, newItem.getAttributes(), newItem.getPosition()); changes.add(change); } oldIndex++; break; } if (oldIndex >= oldList.size()) { break outer; } } } for (; newIndex < newList.size(); newIndex++) { PositionedCsvItem item = newList.get(newIndex); change = new Change(item.getAttribute(uidIndex), Change.Type.CREATE, headers, item.getAttributes(), item.getPosition()); changes.add(change); } for (; oldIndex < oldList.size(); oldIndex++) { PositionedCsvItem item = oldList.get(oldIndex); change = new Change(item.getAttribute(uidIndex), Change.Type.DELETE, headers, item.getAttributes(), item.getPosition()); changes.add(change); } return changes; } private boolean isEqual(CsvItem item1, CsvItem item2) { return Arrays.equals(item1.getAttributes().toArray(), item2.getAttributes().toArray()); } private RecordSet createRecordSet(File file) throws IOException { RecordSet recordSet = null; BufferedReader reader = null; try { reader = createReader(file, configuration); List<String> headers = readHeader(reader, linePattern, configuration); int index = headers.indexOf(configuration.getUniqueAttribute()); if (index < 0 || index >= headers.size()) { throw new CSVSchemaException("Header in '" + file.getAbsolutePath() + "' doesn't contain unique attribute '" + configuration.getUniqueAttribute() + "' as defined in configuration."); } Set<PositionedCsvItem> set = new TreeSet<>(new CsvItemComparator(index)); String line; int lineNumber = 1; while ((line = reader.readLine()) != null) { lineNumber++; if (isEmptyOrComment(line)) { continue; } set.add(Utils.createCsvItem(headers, line, lineNumber, linePattern, configuration)); } recordSet = new RecordSet(headers, set); } finally { if (reader != null) { closeReader(reader, null); } } return recordSet; } private void testHeaders(File newFile, File oldFile) throws IOException, CSVSchemaException { List<String> newHeaders = null; List<String> oldHeaders = null; BufferedReader newReader = null; BufferedReader oldReader = null; try { newReader = createReader(newFile, configuration); newHeaders = readHeader(newReader, linePattern, configuration); oldReader = createReader(oldFile, configuration); oldHeaders = readHeader(oldReader, linePattern, configuration); } finally { closeReader(newReader, null); closeReader(oldReader, null); } if (newHeaders == null || oldHeaders == null || !Arrays.equals(newHeaders.toArray(), oldHeaders.toArray())) { throw new CSVSchemaException("Headers in files '" + newFile.getPath() + "' and '" + oldFile.getPath() + "' doesn't match."); } } private class ChangeComparator implements Comparator<Change> { @Override public int compare(Change o1, Change o2) { // first create/modify entries, then delete ones if ((o1.getType() == Change.Type.CREATE || o1.getType() == Change.Type.MODIFY) && o2.getType() == Change.Type.DELETE) { return -1; } if ((o2.getType() == Change.Type.CREATE || o2.getType() == Change.Type.MODIFY) && o1.getType() == Change.Type.DELETE) { return 1; } return Integer.compare(o1.getPosition(), o2.getPosition()); } } }