/*
* Copyright (c) 2014 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is inspired and uses minor parts of the maven-dependency-plugin by Brian Fox.
*/
package com.evolveum.midpoint.tools.schemadist;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.List;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.xml.resolver.Catalog;
import org.apache.xml.resolver.CatalogManager;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.components.io.fileselectors.IncludeExcludeFileSelector;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.evolveum.midpoint.util.DOMUtil;
/**
* @goal schemadist
* @requiresDependencyResolution compile
* @phase package
*/
public class SchemaDistMojo extends AbstractMojo {
/**
* @parameter
*/
private String includes;
/**
* @parameter
*/
private String excludes;
/**
* @parameter default-value="${project.build.directory}/schemadist" required=true
*/
private File outputDirectory;
/**
* @parameter default-value="${project.build.directory}/schemadist-work" required=true
*/
private File workDirectory;
/**
* @parameter
*/
private List<ArtifactItem> artifactItems;
/**
* @parameter default-value="true" required=true
*/
private boolean translateSchemaLocation;
/**
* @parameter default-value="src/main/schemadoc/resources"
*/
private File resourcesDir;
/** @parameter default-value="${project}" */
private org.apache.maven.project.MavenProject project;
/** @parameter default-value="${localRepository}" */
private ArtifactRepository local;
/** @parameter default-value="${project.remoteArtifactRepositories}" */
protected List<ArtifactRepository> remoteRepos;
/**
* @component
*/
private ArtifactFactory factory;
/**
* @component
*/
private ArtifactResolver resolver;
/**
* @component
*/
private ArchiverManager archiverManager;
/**
* @component
*/
private MavenProjectHelper projectHelper;
private void processArtifactItems() throws MojoExecutionException, InvalidVersionSpecificationException {
for (ArtifactItem artifactItem : artifactItems) {
if (StringUtils.isEmpty(artifactItem.getVersion())) {
fillMissingArtifactVersion(artifactItem);
}
artifactItem.setArtifact(getArtifact(artifactItem));
}
}
private void fillMissingArtifactVersion(ArtifactItem artifactItem) throws MojoExecutionException {
List<Dependency> deps = project.getDependencies();
List<Dependency> depMngt = project.getDependencyManagement() == null
? Collections.<Dependency>emptyList() : project.getDependencyManagement().getDependencies();
if ( !findDependencyVersion( artifactItem, deps, false )
&& ( project.getDependencyManagement() == null || !findDependencyVersion( artifactItem, depMngt, false ) )
&& !findDependencyVersion( artifactItem, deps, true )
&& ( project.getDependencyManagement() == null || !findDependencyVersion( artifactItem, depMngt, true ) ) )
{
throw new MojoExecutionException(
"Unable to find artifact version of " + artifactItem.getGroupId() + ":" + artifactItem.getArtifactId()
+ " in either dependency list or in project's dependency management." );
}
}
private boolean findDependencyVersion(ArtifactItem artifact, List<Dependency> dependencies, boolean looseMatch) {
for ( Dependency dependency : dependencies ) {
if ( StringUtils.equals( dependency.getArtifactId(), artifact.getArtifactId() )
&& StringUtils.equals( dependency.getGroupId(), artifact.getGroupId() )
&& ( looseMatch || StringUtils.equals( dependency.getClassifier(), artifact.getClassifier() ) )
&& ( looseMatch || StringUtils.equals( dependency.getType(), artifact.getType() ) ) ) {
artifact.setVersion( dependency.getVersion() );
return true;
}
}
return false;
}
protected Artifact getArtifact(ArtifactItem artifactItem) throws MojoExecutionException, InvalidVersionSpecificationException {
Artifact artifact;
VersionRange vr = VersionRange.createFromVersionSpec(artifactItem.getVersion());
if (StringUtils.isEmpty(artifactItem.getClassifier())) {
artifact = factory.createDependencyArtifact( artifactItem.getGroupId(), artifactItem.getArtifactId(), vr,
artifactItem.getType(), null, Artifact.SCOPE_COMPILE );
} else {
artifact = factory.createDependencyArtifact( artifactItem.getGroupId(), artifactItem.getArtifactId(), vr,
artifactItem.getType(), artifactItem.getClassifier(),
Artifact.SCOPE_COMPILE );
}
try {
resolver.resolve(artifact, remoteRepos, local);
} catch (ArtifactResolutionException | ArtifactNotFoundException e) {
throw new MojoExecutionException("Error resolving artifact "+artifact, e);
}
return artifact;
}
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info( "SchemaDist plugin started" );
try {
processArtifactItems();
} catch (InvalidVersionSpecificationException e) {
handleFailure(e);
}
final File outDir = initializeOutDir(outputDirectory);
CatalogManager catalogManager = new CatalogManager();
catalogManager.setVerbosity(999);
for (ArtifactItem artifactItem: artifactItems) {
Artifact artifact = artifactItem.getArtifact();
getLog().info( "SchemaDist unpacking artifact " + artifact);
File workDir = new File(workDirectory, artifact.getArtifactId());
initializeOutDir(workDir);
artifactItem.setWorkDir(workDir);
unpack(artifactItem, workDir);
if (translateSchemaLocation) {
String catalogPath = artifactItem.getCatalog();
if (catalogPath != null) {
File catalogFile = new File(workDir, catalogPath);
if (!catalogFile.exists()) {
throw new MojoExecutionException(
"No catalog file " + catalogPath + " in artifact " + artifact);
}
Catalog catalog = new Catalog(catalogManager);
catalog.setupReaders();
try {
// UGLY HACK. On Windows, file names like d:\abc\def\catalog.xml eventually get treated very strangely
// (resulting in names like "file:<current-working-dir>d:\abc\def\catalog.xml" that are obviously wrong)
// Prefixing such names with "file:/" helps.
String prefix;
if (catalogFile.isAbsolute() && !catalogFile.getPath().startsWith("/")) {
prefix = "/";
} else {
prefix = "";
}
String fileName = "file:" + prefix + catalogFile.getPath();
getLog().debug("Calling parseCatalog with: " + fileName);
catalog.parseCatalog(fileName);
} catch (MalformedURLException e) {
throw new MojoExecutionException(
"Error parsing catalog file " + catalogPath + " in artifact " + artifact, e);
} catch (IOException e) {
throw new MojoExecutionException(
"Error parsing catalog file " + catalogPath + " in artifact " + artifact, e);
}
artifactItem.setResolveCatalog(catalog);
}
} else {
getLog().debug("Catalog search disabled for " + artifact);
}
}
for (ArtifactItem artifactItem: artifactItems) {
Artifact artifact = artifactItem.getArtifact();
getLog().info( "SchemaDist processing artifact " + artifact);
final File workDir = artifactItem.getWorkDir();
FileVisitor<Path> fileVisitor = new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
// nothing to do
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) throws IOException {
String fileName = filePath.getFileName().toString();
if (fileName.endsWith(".xsd")) {
getLog().debug("=======================> Processing file "+filePath);
try {
processXsd(filePath, workDir, outDir);
} catch (MojoExecutionException | MojoFailureException e) {
throw new RuntimeException(e.getMessage(),e);
}
} else if (fileName.endsWith(".wsdl")) {
getLog().debug("=======================> Processing file "+filePath);
try {
processWsdl(filePath, workDir, outDir);
} catch (MojoExecutionException | MojoFailureException e) {
throw new RuntimeException(e.getMessage(),e);
}
} else {
getLog().debug("=======================> Skipping file "+filePath);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.TERMINATE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// nothing to do
return FileVisitResult.CONTINUE;
}
};
try {
Files.walkFileTree(workDir.toPath(), fileVisitor);
} catch (IOException e) {
throw new MojoExecutionException("Error processing files of artifact "+artifact, e);
}
}
getLog().info( "SchemaDist plugin finished" );
}
private void processXsd(Path filePath, File workDir, File outDir) throws MojoExecutionException, MojoFailureException {
Document dom = DOMUtil.parseFile(filePath.toFile());
Element rootElement = DOMUtil.getFirstChildElement(dom);
if (translateSchemaLocation) {
processXsdElement(rootElement, filePath, workDir, outDir);
}
serializeXml(dom, filePath, workDir, outDir);
}
private void serializeXml(Document dom, Path filePath, File workDir, File outDir) throws MojoFailureException, MojoExecutionException {
Path fileRelPath = workDir.toPath().relativize(filePath);
File outFile = new File(outDir, fileRelPath.toString());
initializeOutDir(outFile.getParentFile());
try {
DOMUtil.serializeDOMToFile(dom, outFile);
} catch (TransformerFactoryConfigurationError | TransformerException e) {
throw new MojoExecutionException("Error serializing modified file "+fileRelPath+" to XML: "+ e.getMessage(), e);
}
}
private void processXsdElement(Element rootElement, Path filePath, File workDir, File outDir) throws MojoExecutionException, MojoFailureException {
List<Element> importElements = DOMUtil.getChildElements(rootElement, DOMUtil.XSD_IMPORT_ELEMENT);
for (Element importElement: importElements) {
String namespace = DOMUtil.getAttribute(importElement, DOMUtil.XSD_ATTR_NAMESPACE);
if (DOMUtil.getAttribute(importElement, DOMUtil.XSD_ATTR_SCHEMA_LOCATION) == null) {
// target-less imports are skipped
continue;
}
getLog().debug("Processing import of '" + namespace + "'...");
String schemaLocation;
try {
schemaLocation = resolveSchemaLocation(namespace, filePath, workDir);
} catch (IOException e) {
throw new MojoExecutionException(
"Error resolving namespace " + namespace + " in file " + filePath + ": " + e.getMessage(), e);
}
importElement.setAttribute(DOMUtil.XSD_ATTR_SCHEMA_LOCATION.getLocalPart(), schemaLocation);
}
List<Element> includeElements = DOMUtil.getChildElements(rootElement, DOMUtil.XSD_INCLUDE_ELEMENT);
for (Element includeElement: includeElements) {
String schemaLocationOriginal = DOMUtil.getAttribute(includeElement, DOMUtil.XSD_ATTR_SCHEMA_LOCATION);
getLog().debug("Processing include of '" + schemaLocationOriginal + "'...");
String schemaLocationTranslated;
try {
schemaLocationTranslated = resolveSchemaLocation(schemaLocationOriginal, filePath, workDir);
} catch (IOException e) {
throw new MojoExecutionException("Error resolving schemaLocation "+schemaLocationOriginal+" in file "+filePath+": "+ e.getMessage(), e);
}
includeElement.setAttribute(DOMUtil.XSD_ATTR_SCHEMA_LOCATION.getLocalPart(),
schemaLocationTranslated);
}
}
private void processWsdl(Path filePath, File workDir, File outDir) throws MojoExecutionException, MojoFailureException {
Document dom = DOMUtil.parseFile(filePath.toFile());
if (translateSchemaLocation) {
Element rootElement = DOMUtil.getFirstChildElement(dom);
List<Element> importElements = DOMUtil.getChildElements(rootElement, DOMUtil.WSDL_IMPORT_ELEMENT);
for(Element importElement: importElements) {
String namespace = DOMUtil.getAttribute(importElement, DOMUtil.WSDL_ATTR_NAMESPACE);
String schemaLocation;
try {
schemaLocation = resolveSchemaLocation(namespace, filePath, workDir);
} catch (IOException e) {
throw new MojoExecutionException("Error resolving namespace "+namespace+" in file "+filePath+": "+ e.getMessage(), e);
}
importElement.setAttribute(DOMUtil.WSDL_ATTR_LOCATION.getLocalPart(),
schemaLocation);
}
List<Element> typesElements = DOMUtil.getChildElements(rootElement, DOMUtil.WSDL_TYPES_ELEMENT);
for(Element typesElement: typesElements) {
processXsdElement(DOMUtil.getFirstChildElement(typesElement),filePath,workDir,outDir);
}
}
serializeXml(dom, filePath, workDir, outDir);
}
private String resolveSchemaLocation(String namespaceOrLocation, Path filePath, File workDir) throws MojoExecutionException, IOException {
for (ArtifactItem artifactItem: artifactItems) {
Catalog catalog = artifactItem.getResolveCatalog();
if (catalog == null) {
continue;
}
String publicId = namespaceOrLocation;
if (publicId.endsWith("#")) {
publicId = publicId.substring(0, publicId.length()-1);
}
String resolvedString = catalog.resolveEntity(filePath.toString(), publicId, publicId);
if (resolvedString != null) {
getLog().debug("-------------------");
getLog().debug("Resolved namespace/schemaLocation "+namespaceOrLocation+" to "+resolvedString+" using catalog "+catalog);
URL resolvedUrl = new URL(resolvedString);
String resolvedPathString = resolvedUrl.getPath();
Path resolvedPath = new File(resolvedPathString).toPath();
Path workDirPath = workDir.toPath();
Path resolvedRelativeToCatalogWorkdir = artifactItem.getWorkDir().toPath().relativize(resolvedPath);
Path fileRelativeToWorkdir = workDirPath.relativize(filePath);
getLog().debug("workDirPath: "+workDirPath);
getLog().debug("resolvedRelativeToCatalogWorkdir: "+resolvedRelativeToCatalogWorkdir+", fileRelativeToWorkdir: "+fileRelativeToWorkdir);
Path relativePath = fileRelativeToWorkdir.getParent().relativize(resolvedRelativeToCatalogWorkdir);
getLog().debug("Rel: "+relativePath);
String unixSeparators = FilenameUtils.separatorsToUnix(relativePath.toString());
getLog().debug("Normalized to use UNIX separators: " + unixSeparators);
return unixSeparators;
}
}
throw new MojoExecutionException("Cannot resolve namespace "+namespaceOrLocation+" in file "+filePath+" using any of the catalogs");
}
private File initializeOutDir(File dir) throws MojoFailureException {
getLog().info("Output dir: "+dir);
if ( dir.exists() && !dir.isDirectory() ) {
throw new MojoFailureException("Output directory is not a directory: "+dir);
}
if (dir.exists() && !dir.canWrite()) {
throw new MojoFailureException("Output directory is not writable: "+dir);
}
dir.mkdirs();
return dir;
}
private void unpack(ArtifactItem artifactItem, File destDir) throws MojoExecutionException {
Artifact artifact = artifactItem.getArtifact();
File file = artifact.getFile();
if (file == null) {
throw new MojoExecutionException("No file for artifact "+artifact);
}
if (file.isDirectory()) {
try {
FileUtils.copyDirectory(file, destDir);
} catch (IOException e) {
throw new MojoExecutionException("Error copying directory "+file+" to "+destDir+": "+e.getMessage(), e);
}
} else {
try {
UnArchiver unArchiver = archiverManager.getUnArchiver( artifact.getType() );
unArchiver.setSourceFile(file);
unArchiver.setDestDirectory(destDir);
if (StringUtils.isNotEmpty(excludes) || StringUtils.isNotEmpty(includes)) {
// Create the selectors that will filter
// based on include/exclude parameters
// MDEP-47
IncludeExcludeFileSelector[] selectors =
new IncludeExcludeFileSelector[]{ new IncludeExcludeFileSelector() };
if ( StringUtils.isNotEmpty( excludes ) ) {
selectors[0].setExcludes( excludes.split( "," ) );
}
if ( StringUtils.isNotEmpty( includes ) ) {
selectors[0].setIncludes( includes.split( "," ) );
}
unArchiver.setFileSelectors( selectors );
}
unArchiver.extract();
} catch (ArchiverException | NoSuchArchiverException e) {
throw new MojoExecutionException(
"Error unpacking file: " + file + " to: " + destDir + "\r\n" + e.toString(), e );
}
}
}
private void handleFailure(Exception e) throws MojoFailureException {
e.printStackTrace();
throw new MojoFailureException(e.getMessage());
}
}