/**
* Copyright (C) 2013-2014 Olaf Lessenich
* Copyright (C) 2014-2015 University of Passau, Germany
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
* Contributors:
* Olaf Lessenich <lessenic@fim.uni-passau.de>
* Georg Seibt <seibt@fim.uni-passau.de>
*/
package de.fosd.jdime.stats;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.IntSummaryStatistics;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.CollectionConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.ImplicitCollectionMapper;
import de.fosd.jdime.artifact.Artifact;
import de.fosd.jdime.artifact.file.FileArtifact;
import de.fosd.jdime.config.merge.MergeScenario;
import de.fosd.jdime.config.merge.Revision;
import de.fosd.jdime.matcher.matching.LookAheadMatching;
import de.fosd.jdime.matcher.matching.Matching;
/**
* A collection of <code>MergeScenarioStatistics</code> containing collected statistics about
* <code>MergeScenario</code>s that were merged during a run of JDime.
*/
public class Statistics {
private static final Logger LOG = Logger.getLogger(Statistics.class.getCanonicalName());
private static final XStream serializer;
static {
serializer = new XStream();
serializer.setMode(XStream.NO_REFERENCES);
serializer.aliasType(Artifact.class.getSimpleName().toLowerCase(), Artifact.class);
serializer.alias(Revision.class.getSimpleName().toLowerCase(), Revision.class);
serializer.useAttributeFor(Revision.class, "name");
serializer.alias(Statistics.class.getSimpleName().toLowerCase(), Statistics.class);
serializer.omitField(Statistics.class, "currentFileMergeScenario");
serializer.addImplicitMap(Statistics.class, "scenarioStatistics", MergeScenarioStatistics.class, "mergeScenario");
serializer.alias(KeyEnums.Type.class.getSimpleName().toLowerCase(), KeyEnums.Type.class);
serializer.alias(KeyEnums.Level.class.getSimpleName().toLowerCase(), KeyEnums.Level.class);
serializer.alias(Matching.class.getSimpleName().toLowerCase(), Matching.class);
serializer.alias(Matching.class.getSimpleName().toLowerCase(), LookAheadMatching.class);
serializer.omitField(Matching.class, "highlightColor");
serializer.alias(MergeScenarioStatistics.class.getSimpleName().toLowerCase(), MergeScenarioStatistics.class);
for (Field field : ElementStatistics.class.getDeclaredFields()) {
serializer.useAttributeFor(ElementStatistics.class, field.getName());
}
serializer.alias(ElementStatistics.class.getSimpleName().toLowerCase(), ElementStatistics.class);
for (Field field : MergeStatistics.class.getDeclaredFields()) {
serializer.useAttributeFor(MergeStatistics.class, field.getName());
}
serializer.alias(MergeStatistics.class.getSimpleName().toLowerCase(), MergeStatistics.class);
serializer.registerConverter(new Converter() {
private static final String TYPE_ATTR = "type";
private ImplicitCollectionMapper mapper = new ImplicitCollectionMapper(serializer.getMapper());
private CollectionConverter c = new CollectionConverter(mapper);
@Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
MergeScenario<?> mScenario = (MergeScenario<?>) source;
writer.addAttribute(TYPE_ATTR, mScenario.getMergeType().toString());
c.marshal(mScenario.asList(), writer, context);
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
String name = XStream.class.getSimpleName();
String name2 = Statistics.class.getSimpleName();
String msg = String.format("The %s in the %s class can not be used for deserialization.", name, name2);
throw new RuntimeException(msg);
}
@Override
public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
return type.equals(MergeScenario.class);
}
});
serializer.registerConverter(new Converter() {
private static final String SUBCLASS_ATTR = "subclass";
private static final String TYPE_ATTR = "type";
private static final String ID_ATTR = "id";
@Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
Artifact<?> artifact = (Artifact<?>) source;
writer.addAttribute(SUBCLASS_ATTR, artifact.getClass().getSimpleName());
writer.addAttribute(TYPE_ATTR, artifact.getType().name());
writer.addAttribute(ID_ATTR, artifact.getId());
}
@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
String name = XStream.class.getSimpleName();
String name2 = Statistics.class.getSimpleName();
String msg = String.format("The %s in the %s class can not be used for deserialization.", name, name2);
throw new RuntimeException(msg);
}
@Override
public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
return Artifact.class.isAssignableFrom(type);
}
});
}
private MergeScenario<FileArtifact> currentFileMergeScenario;
private Map<MergeScenario<?>, MergeScenarioStatistics> scenarioStatistics;
/**
* Constructs a new <code>Statistics</code> object.
*/
public Statistics() {
this.scenarioStatistics = new HashMap<>();
}
/**
* Copy constructor.
*
* @param toCopy
* the <code>Statistics</code> to copy
*/
public Statistics(Statistics toCopy) {
if (toCopy.currentFileMergeScenario != null) {
this.currentFileMergeScenario = new MergeScenario<>(toCopy.currentFileMergeScenario);
}
this.scenarioStatistics = new HashMap<>();
for (Map.Entry<MergeScenario<?>, MergeScenarioStatistics> entry : toCopy.scenarioStatistics.entrySet()) {
MergeScenario<?> mScenario = new MergeScenario<>(entry.getKey());
MergeScenarioStatistics mStats = new MergeScenarioStatistics(entry.getValue());
this.scenarioStatistics.put(mScenario, mStats);
}
}
/**
* Gets the <code>MergeScenarioStatistics</code> for the current <code>FileArtifact</code>
* <code>MergeScenario</code>.
*
* @return the <code>MergeScenarioStatistics</code>
*/
public MergeScenarioStatistics getCurrentFileMergeScenarioStatistics() {
return getScenarioStatistics(currentFileMergeScenario);
}
/**
* Sets the currently active <code>MergeScenario</code> for <code>FileArtifacts</code> to the new value.
*
* @param currentFileMergeScenario the new <code>MergeScenario</code> for <code>FileArtifacts</code>
*/
public void setCurrentFileMergeScenario(MergeScenario<FileArtifact> currentFileMergeScenario) {
this.currentFileMergeScenario = currentFileMergeScenario;
}
/**
* Checks whether a <code>MergeScenarioStatistics</code> for the given <code>MergeScenario</code> was added to
* this <code>Statistics</code>.
*
* @param mergeScenario
* the <code>MergeScenario</code> to check for
* @return true iff a <code>MergeScenarioStatistics</code> was registered for <code>mergeScenario</code>
*/
public boolean containsStatistics(MergeScenario<?> mergeScenario) {
return scenarioStatistics.containsKey(mergeScenario);
}
/**
* Returns the <code>MergeScenarioStatistics</code> for the given <code>MergeScenario</code>. A new
* <code>MergeScenarioStatistics</code> instance will be created and added if necessary.
*
* @param mergeScenario
* the <code>MergeScenario</code> to get the <code>MergeScenarioStatistics</code> for
* @return the <code>MergeScenarioStatistics</code> for the given <code>MergeScenario</code>
*/
public MergeScenarioStatistics getScenarioStatistics(MergeScenario<?> mergeScenario) {
return scenarioStatistics.computeIfAbsent(mergeScenario, MergeScenarioStatistics::new);
}
/**
* Returns all <code>MergeScenarioStatistics</code> currently added to this <code>Statistics</code> instance.
*
* @return the <code>MergeScenarioStatistics</code>
*/
public List<MergeScenarioStatistics> getScenarioStatistics() {
return scenarioStatistics.values().stream().collect(Collectors.toList());
}
/**
* Adds a <code>MergeScenarioStatistics</code> instance to this <code>Statistics</code>. If there already is a
* <code>MergeScenarioStatistics</code> for the <code>MergeScenario</code> stored in <code>statistics</code> it will
* be added to the old value using {@link MergeScenarioStatistics#add(MergeScenarioStatistics)}.
*
* @param statistics
* the <code>MergeScenarioStatistics</code> to be added
*/
public void addScenarioStatistics(MergeScenarioStatistics statistics) {
scenarioStatistics.merge(statistics.getMergeScenario(), statistics, (o, n) -> {o.add(n); return o;});
}
/**
* Removes the <code>MergeScenarioStatistics</code> for the given <code>scenario</code> from this
* <code>Statistics</code> instance.
*
* @param scenario the <code>MergeScenario</code> whose <code>MergeScenarioStatistics</code> are to be removed.
*/
public void removeScenarioStatistics(MergeScenario<?> scenario) {
scenarioStatistics.remove(scenario);
}
/**
* Returns an <code>IntSummaryStatistics</code> for the conflict ({@link MergeScenarioStatistics#getConflicts()})
* statistics collected in all added <code>MergeScenarioStatistics</code>.
*
* @return the <code>IntSummaryStatistics</code> about conflicts that occurred
*/
public IntSummaryStatistics getConflictStatistics() {
return scenarioStatistics.values().stream().collect(Collectors.summarizingInt(MergeScenarioStatistics::getConflicts));
}
/**
* Returns whether any added <code>MergeScenarioStatistics</code> instance recorded more than 0 conflicts.
*
* @return true iff any added <code>MergeScenarioStatistics</code> recorded conflicts
*/
public boolean hasConflicts() {
return scenarioStatistics.values().stream().anyMatch(s -> s.getConflicts() > 0);
}
/**
* Adds all <code>MergeScenarioStatistics</code> in <code>other</code> to the corresponding
* <code>MergeScenarioStatistics</code> added to <code>this</code>. If a <code>MergeScenarioStatistics</code> in
* <code>other</code> has no partner in <code>this</code> it will simply be added to <code>this</code>.
*
* @param other
* the <code>Statistics</code> to add to <code>this</code>
* @see MergeScenarioStatistics#add(MergeScenarioStatistics)
*/
public void add(Statistics other) {
for (Map.Entry<MergeScenario<?>, MergeScenarioStatistics> entry : other.scenarioStatistics.entrySet()) {
getScenarioStatistics(entry.getKey()).add(entry.getValue());
}
}
/**
* Stores the collected statistics in XML format in the given <code>File</code>. The <code>File</code> will
* be overwritten if it exists.
*
* @param file
* the <code>File</code> to write the statistics to
* @throws FileNotFoundException
* if an exception occurs accessing the <code>File</code>
*/
public void printXML(File file) throws FileNotFoundException {
if (!check(file)) {
return;
}
try (BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
printXML(os);
} catch (IOException e) {
LOG.log(Level.WARNING, e, () -> "Exception while closing an OutputStream.");
}
}
/**
* Writes the collected statistics in XML format to the given <code>OutputStream</code>.
*
* @param os
* the <code>OutputStream</code> to write to
*/
public void printXML(OutputStream os) {
serializer.toXML(this, os);
}
/**
* Writes a human readable representation of the collected statistics to the given <code>File</code>.
*
* @param file
* the <code>File</code> to write the statistics to
* @throws FileNotFoundException
* if an exception occurs accessing the <code>File</code>
*/
public void print(File file) throws FileNotFoundException {
if (!check(file)) {
return;
}
try (PrintStream ps = new PrintStream(new BufferedOutputStream(new FileOutputStream(file)))) {
print(ps);
}
}
/**
* Writes a human readable representation of the collected statistics to the given <code>PrintStream</code>.
*
* @param ps
* the <code>PrintStream</code> to write to
*/
public void print(PrintStream ps) {
for (Iterator<MergeScenarioStatistics> it = getScenarioStatistics().iterator(); it.hasNext(); ) {
it.next().print(ps);
if (it.hasNext()) {
ps.println();
}
}
}
/**
* Checks whether <code>file</code> is a valid file to write to and if necessary creates the directory structure
* above it.
*
* @param file
* the <code>File</code> to check
* @return true iff the file is valid
*/
private boolean check(File file) {
if (file.isDirectory()) {
LOG.warning(() -> file.getAbsolutePath() + " is a directory and can't be written to.");
return false;
}
File parent = file.getAbsoluteFile().getParentFile();
if (parent == null) {
LOG.warning(() -> file.getAbsolutePath() + " does not have a parent directory.");
return false;
}
if (!(parent.exists() || parent.mkdirs())) {
LOG.warning(() -> "Could not create the directory structure for " + parent.getAbsolutePath());
return false;
}
return true;
}
}