/*******************************************************************************
* Copyright (c) 2012, 2014 Google, Inc and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Sergey Prigogin (Google) - initial API and implementation
*******************************************************************************/
package org.eclipse.cdt.internal.ui.refactoring.includes;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.ui.IMemento;
import org.eclipse.cdt.internal.corext.codemanipulation.IncludeInfo;
/**
* A set of header file substitution rules.
*/
public class IncludeMap {
private static final String TAG_MAPPING = "mapping"; //$NON-NLS-1$
private static final String TAG_KEY = "key"; //$NON-NLS-1$
private static final String TAG_VALUE = "value"; //$NON-NLS-1$
private final boolean unconditionalSubstitution; // Not serialized when saving to a memento.
// The order is not crucial but can make a difference when calculating transitive closure.
private final LinkedHashMap<IncludeInfo, List<IncludeInfo>> map;
public IncludeMap(boolean unconditionalSubstitution) {
this.unconditionalSubstitution = unconditionalSubstitution;
this.map = new LinkedHashMap<>();
}
/**
* @param keysAndValues an array of keys and values: [key1, value1, key2, value2, ...].
* Keys and values may be optionally surrounded by double quotes or angle brackets.
* Angle brackets indicate a system include.
*/
public IncludeMap(boolean unconditionalSubstitution, String[] keysAndValues) {
if (keysAndValues.length % 2 != 0)
throw new IllegalArgumentException("More keys than values"); //$NON-NLS-1$
this.unconditionalSubstitution = unconditionalSubstitution;
this.map = new LinkedHashMap<>(keysAndValues.length / 2);
for (int i = 0; i < keysAndValues.length;) {
String key = keysAndValues[i++];
addMapping(key, keysAndValues[i++]);
}
}
public IncludeMap(IncludeMap other) {
this.unconditionalSubstitution = other.unconditionalSubstitution;
this.map = new LinkedHashMap<>(other.map.size());
addAllMappings(other);
}
/**
* Indicates that header file {@code to} should be used instead of {@code from}.
*
* @param from The header file to be replaced.
* @param to The header file to be used.
*/
protected void addMapping(IncludeInfo from, IncludeInfo to) {
if (from.equals(to))
return; // Don't allow mapping to itself.
List<IncludeInfo> list = map.get(from);
if (list == null) {
list = new ArrayList<>(2);
map.put(from, list);
}
list.add(to);
}
/**
* Indicates that header file {@code to} should be used instead of {@code from}.
* @param from The header file to be replaced. The header is represented by an include name
* optionally surrounded by double quotes or angle brackets. Angle brackets indicate
* a system include.
* @param to The header file to be used. The header is represented by an include name optionally
* surrounded by double quotes or angle brackets. Angle brackets indicate a system include.
*/
public void addMapping(String from, String to) {
addMapping(new IncludeInfo(from), new IncludeInfo(to));
}
/**
* Returns header files that should be used instead of the given one.
*
* @param from The header file to be replaced. A system header has to match exactly.
* A non-system header matches both, non-system and system headers.
* @return The list of header files ordered by decreasing preference.
*/
public List<IncludeInfo> getMapping(IncludeInfo from) {
List<IncludeInfo> list = map.get(from);
if (list == null) {
if (!from.isSystem())
list = map.get(new IncludeInfo(from.getName(), true));
if (list == null)
return Collections.emptyList();
}
return list;
}
/**
* Removes substitutions for a given header file.
*
* @param from the header file to remove substitutions for
* @return the previous substitutions associated with the header file, or
* {@code null} if there were no substitutions for the header.
*/
public List<IncludeInfo> removeMapping(String from) {
return removeMapping(new IncludeInfo(from));
}
/**
* Removes substitutions for a given header file.
*
* @param from the header file to remove substitutions for
* @return the previous substitutions associated with the header file, or
* {@code null} if there were no substitutions for the header.
*/
public List<IncludeInfo> removeMapping(IncludeInfo from) {
return map.remove(from);
}
/**
* Returns header files that should be used instead of the given one.
*
* @param from The header file to be replaced. A system header has to match exactly.
* A non-system header matches both, non-system and system headers.
* @return The list of header files ordered by decreasing preference.
*/
public List<IncludeInfo> getMapping(String from) {
return getMapping(new IncludeInfo(from));
}
public IncludeInfo getPreferredMapping(IncludeInfo from) {
List<IncludeInfo> list = getMapping(from);
return list == null ? null : list.get(0);
}
public boolean isUnconditionalSubstitution() {
return unconditionalSubstitution;
}
/**
* Writes the map to a memento. The {@link #isUnconditionalSubstitution()} flag is not written.
*/
public void saveToMemento(IMemento memento) {
List<IncludeInfo> keys = new ArrayList<>(map.keySet());
Collections.sort(keys);
for (IncludeInfo key : keys) {
for (IncludeInfo value : map.get(key)) {
IMemento mapping = memento.createChild(TAG_MAPPING);
mapping.putString(TAG_KEY, key.toString());
mapping.putString(TAG_VALUE, value.toString());
}
}
}
public static IncludeMap fromMemento(boolean unconditionalSubstitution, IMemento memento) {
IncludeMap includeMap = new IncludeMap(unconditionalSubstitution);
Set<String> keys = unconditionalSubstitution ? new HashSet<String>() : Collections.<String>emptySet();
for (IMemento mapping : memento.getChildren(TAG_MAPPING)) {
String key = mapping.getString(TAG_KEY);
// There can be no more than one unconditional substitution for any header file.
if (!unconditionalSubstitution || keys.add(key))
includeMap.addMapping(key, mapping.getString(TAG_VALUE));
}
return includeMap;
}
public void addAllMappings(IncludeMap other) {
if (other.unconditionalSubstitution != unconditionalSubstitution)
throw new IllegalArgumentException();
for (Entry<IncludeInfo, List<IncludeInfo>> entry : other.map.entrySet()) {
IncludeInfo source = entry.getKey();
List<IncludeInfo> otherTargets = entry.getValue();
List<IncludeInfo> targets = map.get(source);
if (targets == null) {
targets = new ArrayList<>(otherTargets);
map.put(source, targets);
} else {
targets.addAll(otherTargets);
}
}
}
public void transitivelyClose() {
for (Entry<IncludeInfo, List<IncludeInfo>> entry : map.entrySet()) {
IncludeInfo source = entry.getKey();
List<IncludeInfo> targets = entry.getValue();
ArrayDeque<IncludeInfo> queue = new ArrayDeque<>(targets);
targets.clear();
HashSet<IncludeInfo> processed = new HashSet<>();
if (!unconditionalSubstitution)
processed.add(source); // Don't allow mapping to itself.
HashSet<IncludeInfo> seenTargets = new HashSet<>();
IncludeInfo target;
queueLoop: while ((target = queue.pollFirst()) != null) {
if (processed.contains(target))
continue;
List<IncludeInfo> newTargets = map.get(target);
if (newTargets != null) {
queue.addFirst(target);
boolean added = false;
// Check if we saw the same target earlier to protect against an infinite loop.
if (seenTargets.add(target)) {
for (int i = newTargets.size(); --i >= 0;) {
IncludeInfo newTarget = newTargets.get(i);
if (!processed.contains(newTarget)) {
if (unconditionalSubstitution && newTarget.equals(source)) {
break queueLoop; // Leave the mapping empty.
}
queue.addFirst(newTarget);
added = true;
}
}
}
if (!added) {
target = queue.pollFirst();
targets.add(target);
if (unconditionalSubstitution)
break;
processed.add(target);
seenTargets.clear();
}
} else {
targets.add(target);
if (unconditionalSubstitution)
break;
processed.add(target);
seenTargets.clear();
}
}
}
if (unconditionalSubstitution) {
// Remove trivial mappings.
for (Iterator<Entry<IncludeInfo, List<IncludeInfo>>> iter = map.entrySet().iterator(); iter.hasNext();) {
Entry<IncludeInfo, List<IncludeInfo>> entry = iter.next();
IncludeInfo source = entry.getKey();
List<IncludeInfo> targets = entry.getValue();
if (targets.isEmpty() || (targets.size() == 1 && source.equals(targets.get(0)))) {
iter.remove();
}
}
}
}
/** For debugging only. */
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append("upconditionalSubstitution = ").append(unconditionalSubstitution); //$NON-NLS-1$
ArrayList<IncludeInfo> sources = new ArrayList<>(map.keySet());
Collections.sort(sources);
for (IncludeInfo source : sources) {
buf.append('\n');
buf.append(source);
buf.append(" -> "); //$NON-NLS-1$
List<IncludeInfo> targets = map.get(source);
for (int i = 0; i < targets.size(); i++) {
if (i > 0)
buf.append(", "); //$NON-NLS-1$
buf.append(targets.get(i));
}
}
return buf.toString();
}
public Map<IncludeInfo, List<IncludeInfo>> getMap() {
return Collections.unmodifiableMap(map);
}
}