package org.jboss.windup.graph.traversal;
import com.tinkerpop.blueprints.Vertex;
import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.graph.model.DuplicateProjectModel;
import org.jboss.windup.graph.model.ProjectModel;
import org.jboss.windup.graph.model.resource.FileModel;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* This allows a {@link ProjectModel} to be traversed in a way that is aware of {@link DuplicateProjectModel}s.
*
* This should make it easier to calculate the actual path within the application without regard to where the original
* project was actually stored.
*
* Cases where this is used include:
* <ul>
* <li>Listing all of the files in the application with accurate paths, regardless of how they are stored in the graph</li>
* <li>The application details report, which traverses the applications in a hierarchical manner.</li>
* </ul>
*
* @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a>
* @author <a href="http://ondra.zizka.cz/">Ondrej Zizka, zizka@seznam.cz</a>
*/
public class ProjectModelTraversal
{
public enum TraversalState
{
/**
* Traverse this node and all children.
*/
ALL,
/**
* Skip the current node, but do include the children
*/
CHILDREN_ONLY,
/**
* Do not traverse this node and also do not traverse the children.
*/
NONE
}
private final ProjectModelTraversal previous;
private final ProjectModel current;
private final TraversalStrategy traversalStrategy;
/**
* Builds an instance with a reference to the previous {@link ProjectModelTraversal}, and the given parameters.
*
* NOTE: This would generally only be used by {@link TraversalStrategy} implementations. {@see DefaultTraversalStrategy}
*/
public ProjectModelTraversal(ProjectModelTraversal previous, ProjectModel current, TraversalStrategy traversalStrategy)
{
this.previous = previous;
this.current = current;
this.traversalStrategy = traversalStrategy == null ? new AllTraversalStrategy() : traversalStrategy;
}
/**
* Creates a new {@link ProjectModelTraversal} based upon the provided {@link ProjectModel}. The {@link ProjectModel}
* should be a "root" model (an application) rather than a subpart of an application.
*
* {@link TraversalStrategy} will default to {@link AllTraversalStrategy}.
*/
public ProjectModelTraversal(ProjectModel current)
{
this(null, current, null);
}
/**
* Creates a new {@link ProjectModelTraversal} based upon the provided {@link ProjectModel}. The {@link ProjectModel}
* should be a "root" model (an application) rather than a subpart of an application.
*
* The provided {@link TraversalStrategy} will determine how the ProjectModel's subprojects tree will be traversed.
*/
public ProjectModelTraversal(ProjectModel current, TraversalStrategy traversalStrategy)
{
this(null, current, traversalStrategy);
}
/**
* Recursively crawls this {@link ProjectModelTraversal} and adds all of the {@link ProjectModel}s to a
* {@link Set}. The exact projects returns will be affected by the {@link TraversalStrategy} in use by the
* current traversal.
*/
public Set<ProjectModel> getAllProjects(boolean recursive)
{
if (!recursive)
return Collections.singleton(getCanonicalProject());
else
return addProjects(new HashSet<ProjectModel>(), this);
}
/**
* Recursively crawls this {@link ProjectModelTraversal} and adds all of the {@link ProjectModel}s to a
* {@link Set}. The exact projects returns will be affected by the {@link TraversalStrategy} in use by the
* current traversal.
*
* This is the same as {@link ProjectModelTraversal#getAllProjects(boolean)} except that it returns a Set of vertices
* instead of frames.
*/
public Set<Vertex> getAllProjectsAsVertices(boolean recursive)
{
Set<Vertex> vertices = new LinkedHashSet<>();
for (ProjectModel projectModel : getAllProjects(recursive))
vertices.add(projectModel.asVertex());
return vertices;
}
private Set<ProjectModel> addProjects(Set<ProjectModel> existingVertices, ProjectModelTraversal traversal)
{
TraversalState nodeTraversalState = traversal.getTraversalState();
if (nodeTraversalState == TraversalState.ALL)
existingVertices.add(traversal.getCanonicalProject());
if (nodeTraversalState != TraversalState.NONE)
{
for (ProjectModelTraversal child : traversal.getChildren())
addProjects(existingVertices, child);
}
return existingVertices;
}
/**
* Implements the Visitor pattern with a callback that receives each {@link ProjectModelTraversal}
* instance. This can be useful for implementing algorithms that need to operate over all of the data within
* this traversal.
*
* Note that the children visited will be affected by the current {@link TraversalStrategy} selected for this traversal.
*/
public void accept(ProjectTraversalVisitor visitor)
{
visitor.visit(this);
for (ProjectModelTraversal child : getChildren())
child.accept(visitor);
}
/**
* Gets all child projects of the current project.
*/
public Iterable<ProjectModelTraversal> getChildren()
{
return traversalStrategy.getChildren(this);
}
/**
* Gets the path of the specified {@link FileModel} within this traversal. The file must be within the
* current {@link ProjectModel} for this method to return an accurate path.
*/
public String getFilePath(FileModel fileModel)
{
FileModel rootFileModel = getCurrent().getRootFileModel();
FileModel canonicalRootFileModel = getCanonicalProject().getRootFileModel();
String base = "";
// get the path from the chain up until this project
if (previous != null)
base = combinePaths(base, previous.getFilePath(canonicalRootFileModel));
// get the path of the root file within its project
if (current.getRootFileModel().getParentFile() != null)
base = combinePaths(base, current.getRootFileModel().getParentFile().getPrettyPathWithinProject());
String rootFilename = rootFileModel.getFileName();
base = combinePaths(base, rootFilename);
// if this is the root file, then just return the base
if (getCurrent().getRootFileModel().getFilePath().equals(fileModel.getFilePath()))
return base;
String relativePath = fileModel.getPrettyPathWithinProject();
return combinePaths(base, relativePath);
}
private String combinePaths(String path1, String path2)
{
if (StringUtils.isNotBlank(path1) && StringUtils.isNotBlank(path2))
return path1 + "/" + path2;
else
return path1 + path2;
}
/**
* Returns a status indicating whether or not this should skip certain parts of the traversal for this node.
*/
public TraversalState getTraversalState()
{
TraversalState calculatedState = traversalStrategy.getTraversalState(this);
return calculatedState == null ? TraversalState.ALL : calculatedState;
}
/**
* Gets the canonical Project by unwrapping any {@link DuplicateProjectModel}s wrapping it.
*/
public ProjectModel getCanonicalProject()
{
return getCanonicalProject(current);
}
/**
* Gets the current {@link ProjectModel} without unwrapping.
*/
public ProjectModel getCurrent()
{
return this.current;
}
private ProjectModel getCanonicalProject(ProjectModel projectModel)
{
if (projectModel instanceof DuplicateProjectModel)
{
DuplicateProjectModel duplicate = (DuplicateProjectModel) projectModel;
return getCanonicalProject(duplicate.getCanonicalProject());
}
else
{
return projectModel;
}
}
@Override
public String toString()
{
FileModel rootFileModel = current == null ? null : current.getRootFileModel();
String checksum = rootFileModel == null ? null : StringUtils.substring(rootFileModel.getMD5Hash(), 0, 8);
String name = rootFileModel == null ? current.getName() : rootFileModel.getFileName();
String projectInfo = current == null ? null : checksum + " " + name + " (" + current.getProjectType() + ')';
String strategyInfo = traversalStrategy == null ? null : traversalStrategy.getClass().getSimpleName();
return "Trav@" + this.hashCode() + "{cur: " + projectInfo + ", strategy: " + strategyInfo + ", prev: " + previous + '}';
}
/**
* Resets the internal state of this traversal, so it can be reused.
*/
public void reset()
{
this.traversalStrategy.reset();
}
}