/*
* Copyright (C) 2013 University of Dundee & Open Microscopy Environment.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package ome.services.blitz.repo.path;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
/**
* Capture a set of rules by which local files may not be named on the file-system.
*
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.0
*/
public class FilePathRestrictions {
private static final ImmutableSet<Integer> controlCodePoints;
private static final Predicate<Integer> isNotControlCodePoint;
/* the full rules */
public final ImmutableSetMultimap<Integer, Integer> transformationMatrix; /* values never empty */
public final ImmutableSet<String> unsafePrefixes;
public final ImmutableSet<String> unsafeSuffixes;
public final ImmutableSet<String> unsafeNames;
public final ImmutableSet<Character> safeCharacters; /* never empty */
/* quick lookups to characters that satisfy the above rules */
public final char safeCharacter;
public final ImmutableMap<Integer, Integer> transformationMap;
static {
final ImmutableSet.Builder<Integer> controlCodePointsBuilder = ImmutableSet.builder();
for (int codePoint = 0; codePoint < 0x100; codePoint++) {
if (Character.getType(codePoint) == Character.CONTROL) {
controlCodePointsBuilder.add(codePoint);
}
}
controlCodePoints = controlCodePointsBuilder.build();
isNotControlCodePoint = new Predicate<Integer>() {
public boolean apply(Integer codePoint) {
return !controlCodePoints.contains(codePoint);
}};
}
/**
* Minimally adjust a set of rules to include transformations away from Unicode control characters.
* @param rules a set of rules
* @return the given rules with full coverage for preventing control characters
*/
private static FilePathRestrictions includeControlTransformations(FilePathRestrictions rules) {
final Set<Character> safeCharacters = new HashSet<Character>(rules.safeCharacters.size());
final Set<Integer> safeCodePoints = new HashSet<Integer>(rules.safeCharacters.size());
for (final Character safeCharacter : rules.safeCharacters) {
final int safeCodePoint = FilePathRestrictionInstance.getCodePoint(safeCharacter);
if (!controlCodePoints.contains(safeCodePoint)) {
safeCharacters.add(safeCharacter);
safeCodePoints.add(safeCodePoint);
}
}
final SetMultimap<Integer, Integer> newTransformationMatrix =
HashMultimap.create(Multimaps.filterValues(rules.transformationMatrix, isNotControlCodePoint));
for (final int controlCodePoint : controlCodePoints) {
if (!newTransformationMatrix.containsKey(controlCodePoint)) {
if (rules.transformationMatrix.containsKey(controlCodePoint)) {
throw new IllegalArgumentException(
"only control character mappings available for Unicode code point " + controlCodePoint);
}
newTransformationMatrix.putAll(controlCodePoint, safeCodePoints);
}
}
return combineRules(rules, new FilePathRestrictions(newTransformationMatrix, null, null, null, safeCharacters));
}
/**
* Combine sets of rules to form a set that satisfies them all.
* @param rules at least one set of rules
* @return the intersection of the given rules
*/
private static FilePathRestrictions combineRules(FilePathRestrictions... rules) {
if (rules.length == 0) {
throw new IllegalArgumentException("cannot combine an empty list of rules");
}
int index = 0;
FilePathRestrictions product = rules[index++];
while (index < rules.length) {
final FilePathRestrictions toCombine = rules[index++];
final Set<Character> safeCharacters = Sets.intersection(product.safeCharacters, toCombine.safeCharacters);
if (safeCharacters.isEmpty()) {
throw new IllegalArgumentException("cannot combine safe characters");
}
final Set<Integer> allKeys = Sets.union(product.transformationMatrix.keySet(), toCombine.transformationMatrix.keySet());
final ImmutableMap<Integer, Collection<Integer>> productMatrixMap = product.transformationMatrix.asMap();
final ImmutableMap<Integer, Collection<Integer>> toCombineMatrixMap = toCombine.transformationMatrix.asMap();
final SetMultimap<Integer, Integer> newTransformationMatrix = HashMultimap.create();
for (final Integer key : allKeys) {
final Collection<Integer> values;
if (!productMatrixMap.containsKey(key)) {
values = toCombineMatrixMap.get(key);
} else if (!toCombineMatrixMap.containsKey(key)) {
values = productMatrixMap.get(key);
} else {
final Set<Integer> valuesSet = new HashSet<Integer>(productMatrixMap.get(key));
valuesSet.retainAll(toCombineMatrixMap.get(key));
if (valuesSet.isEmpty()) {
throw new IllegalArgumentException("cannot combine transformations for Unicode code point " + key);
}
values = valuesSet;
}
for (final Integer value : values) {
newTransformationMatrix.put(key, value);
}
}
final SetMultimap<Integer, Integer> entriesRemoved = HashMultimap.create();
boolean transitiveClosing;
do {
transitiveClosing = false;
for (final Entry<Integer, Integer> transformation : newTransformationMatrix.entries()) {
final int to = transformation.getValue();
if (newTransformationMatrix.containsKey(to)) {
final int from = transformation.getKey();
if (!entriesRemoved.put(from, to)) {
throw new IllegalArgumentException("cyclic transformation involving Unicode code point " + from);
}
newTransformationMatrix.remove(from, to);
newTransformationMatrix.putAll(from, newTransformationMatrix.get(to));
transitiveClosing = true;
break;
}
}
} while (transitiveClosing);
product = new FilePathRestrictions(newTransformationMatrix,
Sets.union(product.unsafePrefixes, toCombine.unsafePrefixes),
Sets.union(product.unsafeSuffixes, toCombine.unsafeSuffixes),
Sets.union(product.unsafeNames, toCombine.unsafeNames),
safeCharacters);
}
return product;
}
/**
* Combine sets of rules to form a set that satisfies them all and that
* include transformations away from Unicode control characters.
* @param rules at least one set of rules
* @return the intersection of the given rules, with full coverage for preventing control characters
*/
public static FilePathRestrictions combineFilePathRestrictions(FilePathRestrictions... rules) {
return includeControlTransformations(combineRules(rules));
}
/**
* Construct a set of rules by which local files may not be named on the file-system.
* @param transformationMatrix how to make specific characters safe, may be null
* @param unsafePrefixes which name prefixes are proscribed, may be null
* @param unsafeSuffixes which name suffixes are proscribed, may be null
* @param unsafeNames which names are proscribed, may be null
* @param safeCharacters safe characters that may be used in making file names safe, may <em>not</em> be null
*/
public FilePathRestrictions(SetMultimap<Integer, Integer> transformationMatrix,
Set<String> unsafePrefixes, Set<String> unsafeSuffixes, Set<String> unsafeNames,
Set<Character> safeCharacters) {
this.transformationMatrix = transformationMatrix == null ? ImmutableSetMultimap.<Integer, Integer>of()
: ImmutableSetMultimap.copyOf(transformationMatrix);
this.unsafePrefixes = unsafePrefixes == null ? ImmutableSet.<String>of() : ImmutableSet.copyOf(unsafePrefixes);
this.unsafeSuffixes = unsafeSuffixes == null ? ImmutableSet.<String>of() : ImmutableSet.copyOf(unsafeSuffixes);
this.unsafeNames = unsafeNames == null ? ImmutableSet.<String>of() : ImmutableSet.copyOf(unsafeNames);
this.safeCharacters = ImmutableSet.copyOf(safeCharacters);
this.safeCharacter = this.safeCharacters.iterator().next();
int safeCodePoint = FilePathRestrictionInstance.getCodePoint(this.safeCharacter);
final ImmutableMap.Builder<Integer, Integer> transformationMapBuilder = ImmutableMap.builder();
for (final Entry<Integer, Collection<Integer>> transformation : this.transformationMatrix.asMap().entrySet()) {
final Collection<Integer> values = transformation.getValue();
final Integer selectedValue = values.contains(safeCodePoint) ? safeCodePoint : values.iterator().next();
transformationMapBuilder.put(transformation.getKey(), selectedValue);
}
this.transformationMap = transformationMapBuilder.build();
}
}