/*
* Copyright (C) 2014-2016 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 omero.cmd.graphs;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.hibernate.Session;
import ome.model.core.OriginalFile;
import ome.model.internal.Details;
import ome.services.graphs.GraphException;
import ome.services.graphs.GraphPolicy.Ability;
import ome.services.graphs.GraphTraversal.Processor;
import ome.services.graphs.GraphOpts.Op;
import ome.services.graphs.ModelObjectSequencer;
import omero.cmd.GraphModify2;
import omero.cmd.Request;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.SetMultimap;
/**
* Static utility methods for model graph operations.
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.1.0
*/
public class GraphUtil {
/**
* Split a list of strings by a given separator, trimming whitespace and ignoring empty items.
* @param separator the separator between the list items
* @param list the list
* @return a means of iterating over the list items
*/
private static Iterable<String> splitList(char separator, String list) {
return Splitter.on(separator).trimResults().omitEmptyStrings().split(list);
}
/**
* Copy the {@link GraphModify2} fields of one request to another.
* @param requestFrom the source of the field copy
* @param requestTo the target of the field copy
*/
static void copyFields(GraphModify2 requestFrom, GraphModify2 requestTo) {
if (requestFrom.targetObjects == null) {
requestTo.targetObjects = null;
} else {
requestTo.targetObjects = new HashMap<String, List<Long>>();
for (final Map.Entry<String, List<Long>> targetObjectsOneClass : requestFrom.targetObjects.entrySet()) {
final String targetClass = targetObjectsOneClass.getKey();
final List<Long> targetIds = targetObjectsOneClass.getValue();
requestTo.targetObjects.put(targetClass, new ArrayList<Long>(targetIds));
}
}
if (requestFrom.childOptions == null) {
requestTo.childOptions = null;
} else {
requestTo.childOptions = new ArrayList<ChildOption>(requestFrom.childOptions);
}
requestTo.dryRun = requestFrom.dryRun;
}
/**
* Approximately translate {@link GraphModify} options in setting the parameters of a {@link GraphModify2} request.
* @param graphRequestFactory a means of instantiating new child options
* @param options {@link GraphModify} options, may be {@code null}
* @param request the request whose options should be updated
* @param isAdmin if the current user is a system administrator
* @throws GraphException if a non-administrator attempted to use {@link Op#FORCE}
* @deprecated because facade classes are deprecated
*/
@Deprecated
static void translateOptions(GraphRequestFactory graphRequestFactory, Map<String, String> options,
GraphModify2 request, boolean isAdmin) throws GraphException {
if (options == null) {
return;
}
final List<ChildOption> childOptions = new ArrayList<ChildOption>(options.size());
for (final Map.Entry<String, String> option : options.entrySet()) {
final ChildOption childOption = graphRequestFactory.createChildOption();
/* find type to which options apply */
String optionType = option.getKey();
if (optionType.charAt(0) == '/') {
optionType = optionType.substring(1);
}
boolean notedType = false;
for (final String optionValue : GraphUtil.splitList(';', option.getValue())) {
/* approximately translate each option */
if (Op.KEEP.toString().equals(optionValue)) {
childOption.excludeType = Collections.singletonList(optionType);
notedType = true;
} else if (Op.HARD.toString().equals(optionValue)) {
childOption.includeType = Collections.singletonList(optionType);
notedType = true;
} else if (Op.FORCE.toString().equals(optionValue)) {
if (!isAdmin) {
throw new GraphException("only administrators may specify " + Op.FORCE);
}
} else if (optionValue.startsWith("excludes=")) {
if (childOption.excludeNs == null) {
childOption.excludeNs = new ArrayList<String>();
}
for (final String namespace : GraphUtil.splitList(',', optionValue.substring(9))) {
childOption.excludeNs.add(namespace);
}
}
}
/* each child option must apply to specific types */
if (notedType) {
childOptions.add(childOption);
}
}
request.childOptions = childOptions.isEmpty() ? null : childOptions;
}
/**
* Make a copy of a multimap with the full class names in the keys replaced by the simple class names
* and the ordering of the values preserved.
* @param entriesByFullName a multimap
* @return a new multimap with the same contents, except for the package name having been trimmed off each key
*/
static <X> SetMultimap<String, X> trimPackageNames(SetMultimap<String, X> entriesByFullName) {
final SetMultimap<String, X> entriesBySimpleName = LinkedHashMultimap.create();
for (final Map.Entry<String, Collection<X>> entriesForOneClass : entriesByFullName.asMap().entrySet()) {
final String fullClassName = entriesForOneClass.getKey();
final String simpleClassName = fullClassName.substring(fullClassName.lastIndexOf('.') + 1);
final Collection<X> values = entriesForOneClass.getValue();
entriesBySimpleName.putAll(simpleClassName, values);
}
return entriesBySimpleName;
}
/**
* Find the first class-name in a {@code /}-separated string.
* @param type a type path in the style of the original graph traversal code
* @return the first type found in the path
* @deprecated because facade classes are deprecated
*/
@Deprecated
static String getFirstClassName(String type) {
while (type.charAt(0) == '/') {
type = type.substring(1);
}
final int firstSlash = type.indexOf('/');
if (firstSlash > 0) {
type = type.substring(0, firstSlash);
}
return type;
}
/**
* Combine consecutive facade requests with the same options into one request with the union of the target model objects.
* Does not adjust {@link GraphModify2} requests because they already allow the caller to specify multiple target model objects
* should they wish those objects to be processed together.
* Call this method before calling {@link omero.cmd.IRequest#init(omero.cmd.Helper)} on the requests.
* @param requests the list of requests to adjust
* @deprecated because facade classes are deprecated
*/
@Deprecated
public static void combineFacadeRequests(List<Request> requests) {
if (requests == null) {
return;
}
int index = 0;
while (index < requests.size() - 1) {
final Request request1 = requests.get(index);
final Request request2 = requests.get(index + 1);
final boolean isCombined;
if (request1 instanceof ChgrpFacadeI && request2 instanceof ChgrpFacadeI) {
isCombined = isCombined((ChgrpFacadeI) request1, (ChgrpFacadeI) request2);
} else if (request1 instanceof ChownFacadeI && request2 instanceof ChownFacadeI) {
isCombined = isCombined((ChownFacadeI) request1, (ChownFacadeI) request2);
} else if (request1 instanceof DeleteFacadeI && request2 instanceof DeleteFacadeI) {
isCombined = isCombined((DeleteFacadeI) request1, (DeleteFacadeI) request2);
} else {
isCombined = false;
}
if (isCombined) {
requests.remove(index + 1);
} else {
index++;
}
}
}
/**
* Test if the maps have the same contents, regardless of ordering.
* {@code null} arguments are taken as being empty maps.
* @param map1 the first map
* @param map2 the second map
* @return if the two maps have the same contents
*/
private static <K, V> boolean isEqualMaps(Map<K, V> map1, Map<K, V> map2) {
if (map1 == null) {
map1 = Collections.emptyMap();
}
if (map2 == null) {
map2 = Collections.emptyMap();
}
return CollectionUtils.isEqualCollection(map1.entrySet(), map2.entrySet());
}
/**
* Combine the two chgrp requests should they be sufficiently similar.
* @param chgrp1 the first request
* @param chgrp2 the second request
* @return if the target model object of the second request was successfully merged into those of the first request
*/
private static boolean isCombined(ChgrpFacadeI chgrp1, ChgrpFacadeI chgrp2) {
if (isEqualMaps(chgrp1.options, chgrp2.options) &&
chgrp1.grp == chgrp2.grp) {
chgrp1.addToTargets(chgrp2.type, chgrp2.id);
return true;
} else {
return false;
}
}
/**
* Combine the two chown requests should they be sufficiently similar.
* @param chown1 the first request
* @param chown2 the second request
* @return if the target model object of the second request was successfully merged into those of the first request
*/
private static boolean isCombined(ChownFacadeI chown1, ChownFacadeI chown2) {
if (isEqualMaps(chown1.options, chown2.options) &&
chown1.user == chown2.user) {
chown1.addToTargets(chown2.type, chown2.id);
return true;
} else {
return false;
}
}
/**
* Combine the two delete requests should they be sufficiently similar.
* @param delete1 the first request
* @param delete2 the second request
* @return if the target model object of the second request was successfully merged into those of the first request
*/
private static boolean isCombined(DeleteFacadeI delete1, DeleteFacadeI delete2) {
/* in deleting original files, order is significant */
if (isEqualMaps(delete1.options, delete2.options)) {
delete1.addToTargets(delete2.type, delete2.id);
return true;
} else {
return false;
}
}
/**
* Copy a multimap to a new map. Useful for constructing Ice-compatible responses from graph requests.
* @param result a result from a graph operation
* @return the result transformed to fit a {@code StringLongListMap} API binding
*/
public static Map<String, List<Long>> copyMultimapForResponse(SetMultimap<String, Long> result) {
/* if the results object were in terms of IObjectList then this would need IceMapper.map */
final Map<String, List<Long>> forResponse = new HashMap<String, List<Long>>();
for (final Map.Entry<String, Collection<Long>> oneClass : result.asMap().entrySet()) {
final String className = oneClass.getKey();
final Collection<Long> ids = oneClass.getValue();
forResponse.put(className, new ArrayList<Long>(ids));
}
return forResponse;
}
/**
* Rearrange the deletion targets such that original files are listed before their containing directories.
* @param session the Hibernate session
* @param targetObjects the objects that are to be deleted
* @return the given target objects with any original files suitably ordered for deletion
*/
static SetMultimap<String, Long> arrangeDeletionTargets(Session session, SetMultimap<String, Long> targetObjects) {
if (targetObjects.get(OriginalFile.class.getName()).size() < 2) {
/* no need to rearrange anything, as there are not multiple original files */
return targetObjects;
}
final SetMultimap<String, Long> orderedIds = LinkedHashMultimap.create();
for (final Map.Entry<String, Collection<Long>> targetObjectsByClass : targetObjects.asMap().entrySet()) {
final String className = targetObjectsByClass.getKey();
Collection<Long> ids = targetObjectsByClass.getValue();
if (OriginalFile.class.getName().equals(className)) {
final Collection<Collection<Long>> sortedIds = ModelObjectSequencer.sortOriginalFileIds(session, ids);
ids = new ArrayList<Long>(ids.size());
for (final Collection<Long> idBatch : sortedIds) {
ids.addAll(idBatch);
}
}
orderedIds.putAll(className, ids);
}
return orderedIds;
}
/**
* Wrap a graph traversal processor so that it has no write effects.
* @param processor a graph traversal processor to wrap
* @return the graph traversal processor wrapped so that it has no write effects
*/
static Processor disableProcessor(final Processor processor) {
return new Processor() {
@Override
public void nullProperties(String className, String propertyName, Collection<Long> ids) {
/* disable this write action */
}
@Override
public void deleteInstances(String className, Collection<Long> ids) {
/* disable this write action */
}
@Override
public void processInstances(String className, Collection<Long> ids) {
/* disable this write action */
}
@Override
public Set<Ability> getRequiredPermissions() {
return processor.getRequiredPermissions();
}
@Override
public void assertMayProcess(String className, long id, Details details) throws GraphException {
processor.assertMayProcess(className, id, details);
}
};
}
/**
* Filter the given value to return only objects from the mapping. If a collection type, recurse into filtering the elements.
* @param mapping a mapping of non-collection elements to replace with others
* @param value the value to filter, may be {@code null}
* @return the mapped objects for the objects found in the given value, never {@code null}
*/
static <X> Set<X> filterComplexValue(Function<Object, X> mapping, Object value) {
return value == null ? Collections.<X>emptySet() : filterComplexValue(new HashSet<X>(), mapping, value);
}
private static <X> Set<X> filterComplexValue(Set<X> sought, Function<Object, X> mapping, Object value) {
if (value instanceof Iterable) {
@SuppressWarnings("unchecked")
final Iterable<Object> iterable = (Iterable<Object>) value;
for (final Object element : iterable) {
filterComplexValue(sought, mapping, element);
}
return sought;
} else if (value instanceof Map) {
@SuppressWarnings("unchecked")
final Map<Object, Object> map = (Map<Object, Object>) value;
for (final Map.Entry<Object, Object> element : map.entrySet()) {
filterComplexValue(sought, mapping, element.getKey());
filterComplexValue(sought, mapping, element.getValue());
}
} else if (value != null) {
final X mapped = mapping.apply(value);
if (mapped != null) {
sought.add(mapped);
}
}
return sought;
}
/**
* Copy the given value. If a collection type, recurse into copying the elements.
* @param mapping a mapping of non-collection elements to replace with others in the copy
* @param value the value to copy, may be {@code null}
* @return a copy of the value with newly instantiated lists, sets and maps
*/
static Object copyComplexValue(Function<Object, ? extends Object> mapping, Object value) {
if (value == null) {
return null;
} else if (value instanceof List) {
@SuppressWarnings("unchecked")
final List<Object> original = (List<Object>) value;
final List<Object> copy = new ArrayList<Object>(original.size());
for (final Object element : original) {
copy.add(copyComplexValue(mapping, element));
}
return copy;
} else if (value instanceof Set) {
@SuppressWarnings("unchecked")
final Set<Object> original = (Set<Object>) value;
final Set<Object> copy = new HashSet<Object>();
for (final Object element : original) {
copy.add(copyComplexValue(mapping, element));
}
return copy;
} else if (value instanceof Map) {
@SuppressWarnings("unchecked")
final Map<Object, Object> original = (Map<Object, Object>) value;
final Map<Object, Object> copy = new HashMap<Object, Object>();
for (final Map.Entry<Object, Object> element : original.entrySet()) {
copy.put(copyComplexValue(mapping, element.getKey()), copyComplexValue(mapping, element.getValue()));
}
return copy;
} else {
final Object mapped = mapping.apply(value);
return mapped == null ? value : mapped;
}
}
/**
* Create a human-readable view of graph request parameters and results to be logged for debugging.
* @author m.t.b.carroll@dundee.ac.uk
* @since 5.2.1
*/
static class ParameterReporter {
private final List<String> parameters = new ArrayList<String>();
/**
* Sort a collection only if it is a {@link List} of {@link Comparable} elements.
* @param collection the collection to sort
*/
private static void possiblySort(Collection<Object> collection) {
if (!(collection instanceof List)) {
return;
}
final List<Comparable<Object>> comparableElements = new ArrayList<Comparable<Object>>(collection.size());
for (Object element : collection) {
if (element instanceof Comparable) {
comparableElements.add((Comparable<Object>) element);
} else {
return;
}
}
Collections.sort(comparableElements);
collection.clear();
for (final Comparable<Object> comparableElement : comparableElements) {
collection.add(comparableElement);
}
}
/**
* Add a parameter to those to be reported.
* @param name the parameter name
* @param values the parameter value
*/
void addParameter(String name, Collection<Object> values) {
if (CollectionUtils.isEmpty(values)) {
return;
}
possiblySort(values);
final List<String> elements = new ArrayList<String>();
for (Object element : values) {
if (element instanceof ChildOption) {
final ChildOption childOption = (ChildOption) element;
final StringBuilder sb = new StringBuilder();
sb.append('<');
final ParameterReporter arguments = new ParameterReporter();
arguments.addParameter("includeType", childOption.includeType);
arguments.addParameter("excludeType", childOption.excludeType);
arguments.addParameter("includeNs", childOption.includeNs);
arguments.addParameter("excludeNs", childOption.excludeNs);
sb.append(arguments);
sb.append('>');
element = sb;
}
elements.add(element.toString());
}
parameters.add(name + " = [" + Joiner.on(',').join(elements) + "]");
}
/**
* Add a parameter to those to be reported.
* @param name the parameter name
* @param values the parameter value
*/
void addParameter(String name, Map<String, List<Long>> values) {
if (MapUtils.isEmpty(values)) {
return;
}
final StringBuilder sb = new StringBuilder();
sb.append(name);
sb.append(" = ");
sb.append('{');
boolean needComma = false;
if (!(values instanceof SortedMap)) {
values = new TreeMap<String, List<Long>>(values);
}
for (final Map.Entry<String, List<Long>> oneClass : values.entrySet()) {
if (needComma) {
sb.append(',');
} else {
needComma = true;
}
sb.append(oneClass.getKey());
sb.append(':');
final List<Long> ids = oneClass.getValue();
Collections.sort(ids);
sb.append('[');
sb.append(Joiner.on(',').join(ids));
sb.append(']');
}
sb.append('}');
parameters.add(sb.toString());
}
/**
* Add a parameter to those to be reported.
* @param name the parameter name
* @param values the parameter value
*/
void addParameter(String name, Object value) {
if (value instanceof Collection) {
addParameter(name, (Collection<Object>) value);
} else if (value instanceof Map) {
this.addParameter(name, (Map<String, List<Long>>) value);
} else if (value != null) {
parameters.add(name + " = " + value);
}
}
@Override
public String toString() {
return Joiner.on(", ").join(parameters);
}
}
}