blob: c6c55d0eeb200259b22ebf46e72702cfe4ebec1b [file] [log] [blame]
package org.apache.maven.doxia.tools;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import org.apache.commons.io.FilenameUtils;
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.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.doxia.site.decoration.Banner;
import org.apache.maven.doxia.site.decoration.DecorationModel;
import org.apache.maven.doxia.site.decoration.Menu;
import org.apache.maven.doxia.site.decoration.MenuItem;
import org.apache.maven.doxia.site.decoration.Skin;
import org.apache.maven.doxia.site.decoration.inheritance.DecorationModelInheritanceAssembler;
import org.apache.maven.doxia.site.decoration.io.xpp3.DecorationXpp3Reader;
import org.apache.maven.doxia.site.decoration.io.xpp3.DecorationXpp3Writer;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Model;
import org.apache.maven.model.Site;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.reporting.MavenReport;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.i18n.I18N;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.MapBasedValueSource;
import org.codehaus.plexus.interpolation.ObjectBasedValueSource;
import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
import org.codehaus.plexus.interpolation.PrefixedPropertiesValueSource;
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
/**
* Default implementation of the site tool.
*
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @version $Id$
*/
@Component( role = SiteTool.class )
public class DefaultSiteTool
extends AbstractLogEnabled
implements SiteTool
{
// ----------------------------------------------------------------------
// Components
// ----------------------------------------------------------------------
/**
* The component that is used to resolve additional artifacts required.
*/
@Requirement
private ArtifactResolver artifactResolver;
/**
* The component used for creating artifact instances.
*/
@Requirement
private ArtifactFactory artifactFactory;
/**
* Internationalization.
*/
@Requirement
protected I18N i18n;
/**
* The component for assembling inheritance.
*/
@Requirement
protected DecorationModelInheritanceAssembler assembler;
/**
* Project builder.
*/
@Requirement
protected MavenProjectBuilder mavenProjectBuilder;
// ----------------------------------------------------------------------
// Public methods
// ----------------------------------------------------------------------
/** {@inheritDoc} */
public Artifact getSkinArtifactFromRepository( ArtifactRepository localRepository,
List<ArtifactRepository> remoteArtifactRepositories,
DecorationModel decoration )
throws SiteToolException
{
checkNotNull( "localRepository", localRepository );
checkNotNull( "remoteArtifactRepositories", remoteArtifactRepositories );
checkNotNull( "decoration", decoration );
Skin skin = decoration.getSkin();
if ( skin == null )
{
skin = Skin.getDefaultSkin();
}
String version = skin.getVersion();
Artifact artifact;
try
{
if ( version == null )
{
version = Artifact.RELEASE_VERSION;
}
VersionRange versionSpec = VersionRange.createFromVersionSpec( version );
artifact = artifactFactory.createDependencyArtifact( skin.getGroupId(), skin.getArtifactId(), versionSpec,
"jar", null, null );
artifactResolver.resolve( artifact, remoteArtifactRepositories, localRepository );
}
catch ( InvalidVersionSpecificationException e )
{
throw new SiteToolException( "InvalidVersionSpecificationException: The skin version '" + version
+ "' is not valid: " + e.getMessage(), e );
}
catch ( ArtifactResolutionException e )
{
throw new SiteToolException( "ArtifactResolutionException: Unable to find skin", e );
}
catch ( ArtifactNotFoundException e )
{
throw new SiteToolException( "ArtifactNotFoundException: The skin does not exist: " + e.getMessage(), e );
}
return artifact;
}
/** {@inheritDoc} */
public Artifact getDefaultSkinArtifact( ArtifactRepository localRepository,
List<ArtifactRepository> remoteArtifactRepositories )
throws SiteToolException
{
return getSkinArtifactFromRepository( localRepository, remoteArtifactRepositories, new DecorationModel() );
}
/** {@inheritDoc} */
public String getRelativePath( String to, String from )
{
checkNotNull( "to", to );
checkNotNull( "from", from );
URL toUrl = null;
URL fromUrl = null;
String toPath = to;
String fromPath = from;
try
{
toUrl = new URL( to );
}
catch ( MalformedURLException e )
{
try
{
toUrl = new File( getNormalizedPath( to ) ).toURI().toURL();
}
catch ( MalformedURLException e1 )
{
getLogger().warn( "Unable to load a URL for '" + to + "': " + e.getMessage() );
}
}
try
{
fromUrl = new URL( from );
}
catch ( MalformedURLException e )
{
try
{
fromUrl = new File( getNormalizedPath( from ) ).toURI().toURL();
}
catch ( MalformedURLException e1 )
{
getLogger().warn( "Unable to load a URL for '" + from + "': " + e.getMessage() );
}
}
if ( toUrl != null && fromUrl != null )
{
// URLs, determine if they share protocol and domain info
if ( ( toUrl.getProtocol().equalsIgnoreCase( fromUrl.getProtocol() ) )
&& ( toUrl.getHost().equalsIgnoreCase( fromUrl.getHost() ) )
&& ( toUrl.getPort() == fromUrl.getPort() ) )
{
// shared URL domain details, use URI to determine relative path
toPath = toUrl.getFile();
fromPath = fromUrl.getFile();
}
else
{
// don't share basic URL information, no relative available
return to;
}
}
else if ( ( toUrl != null && fromUrl == null ) || ( toUrl == null && fromUrl != null ) )
{
// one is a URL and the other isn't, no relative available.
return to;
}
// either the two locations are not URLs or if they are they
// share the common protocol and domain info and we are left
// with their URI information
String relativePath = getRelativeFilePath( fromPath, toPath );
if ( relativePath == null )
{
relativePath = to;
}
if ( getLogger().isDebugEnabled() && !relativePath.toString().equals( to ) )
{
getLogger().debug( "Mapped url: " + to + " to relative path: " + relativePath );
}
return relativePath;
}
private static String getRelativeFilePath( final String oldPath, final String newPath )
{
// normalize the path delimiters
String fromPath = new File( oldPath ).getPath();
String toPath = new File( newPath ).getPath();
// strip any leading slashes if its a windows path
if ( toPath.matches( "^\\[a-zA-Z]:" ) )
{
toPath = toPath.substring( 1 );
}
if ( fromPath.matches( "^\\[a-zA-Z]:" ) )
{
fromPath = fromPath.substring( 1 );
}
// lowercase windows drive letters.
if ( fromPath.startsWith( ":", 1 ) )
{
fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 );
}
if ( toPath.startsWith( ":", 1 ) )
{
toPath = Character.toLowerCase( toPath.charAt( 0 ) ) + toPath.substring( 1 );
}
// check for the presence of windows drives. No relative way of
// traversing from one to the other.
if ( ( toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) )
&& ( !toPath.substring( 0, 1 ).equals( fromPath.substring( 0, 1 ) ) ) )
{
// they both have drive path element but they don't match, no
// relative path
return null;
}
if ( ( toPath.startsWith( ":", 1 ) && !fromPath.startsWith( ":", 1 ) )
|| ( !toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) )
{
// one has a drive path element and the other doesn't, no relative
// path.
return null;
}
final String relativePath = buildRelativePath( toPath, fromPath, File.separatorChar );
return relativePath.toString();
}
/** {@inheritDoc} */
public File getSiteDescriptor( File siteDirectory, Locale locale )
{
checkNotNull( "siteDirectory", siteDirectory );
final Locale llocale = ( locale == null ) ? new Locale( "" ) : locale;
File siteDescriptor = new File( siteDirectory, "site_" + llocale.getLanguage() + ".xml" );
if ( !siteDescriptor.isFile() )
{
siteDescriptor = new File( siteDirectory, "site.xml" );
}
return siteDescriptor;
}
/**
* Get a site descriptor from one of the repositories.
*
* @param project the Maven project, not null.
* @param localRepository the Maven local repository, not null.
* @param repositories the Maven remote repositories, not null.
* @param locale the locale wanted for the site descriptor. If not null, searching for
* <code>site_<i>localeLanguage</i>.xml</code>, otherwise searching for <code>site.xml</code>.
* @return the site descriptor into the local repository after download of it from repositories or null if not
* found in repositories.
* @throws SiteToolException if any
*/
File getSiteDescriptorFromRepository( MavenProject project, ArtifactRepository localRepository,
List<ArtifactRepository> repositories, Locale locale )
throws SiteToolException
{
checkNotNull( "project", project );
checkNotNull( "localRepository", localRepository );
checkNotNull( "repositories", repositories );
final Locale llocale = ( locale == null ) ? new Locale( "" ) : locale;
try
{
return resolveSiteDescriptor( project, localRepository, repositories, llocale );
}
catch ( ArtifactNotFoundException e )
{
getLogger().debug( "ArtifactNotFoundException: Unable to locate site descriptor: " + e );
return null;
}
catch ( ArtifactResolutionException e )
{
throw new SiteToolException( "ArtifactResolutionException: Unable to locate site descriptor: "
+ e.getMessage(), e );
}
catch ( IOException e )
{
throw new SiteToolException( "IOException: Unable to locate site descriptor: " + e.getMessage(), e );
}
}
/**
* Read site descriptor content from Reader, adding support for deprecated <code>${reports}</code>,
* <code>${parentProject}</code> and <code>${modules}</code> tags.
*
* @param reader
* @return the input content interpolated with deprecated tags
* @throws IOException
*/
private String readSiteDescriptor( Reader reader, String projectId )
throws IOException
{
String siteDescriptorContent = IOUtil.toString( reader );
// This is to support the deprecated ${reports}, ${parentProject} and ${modules} tags.
Properties props = new Properties();
props.put( "reports", "<menu ref=\"reports\"/>" );
props.put( "modules", "<menu ref=\"modules\"/>" );
props.put( "parentProject", "<menu ref=\"parent\"/>" );
// warn if interpolation required
for ( Object prop : props.keySet() )
{
if ( siteDescriptorContent.contains( "$" + prop ) )
{
getLogger().warn( "Site descriptor for " + projectId + " contains $" + prop
+ ": should be replaced with " + props.getProperty( (String) prop ) );
}
if ( siteDescriptorContent.contains( "${" + prop + "}" ) )
{
getLogger().warn( "Site descriptor for " + projectId + " contains ${" + prop
+ "}: should be replaced with " + props.getProperty( (String) prop ) );
}
}
return StringUtils.interpolate( siteDescriptorContent, props );
}
/** {@inheritDoc} */
public DecorationModel getDecorationModel( File siteDirectory, Locale locale, MavenProject project,
List<MavenProject> reactorProjects, ArtifactRepository localRepository,
List<ArtifactRepository> repositories )
throws SiteToolException
{
checkNotNull( "project", project );
checkNotNull( "reactorProjects", reactorProjects );
checkNotNull( "localRepository", localRepository );
checkNotNull( "repositories", repositories );
final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale;
getLogger().debug( "Computing decoration model of " + project.getId() + " for locale " + llocale );
Map.Entry<DecorationModel, MavenProject> result =
getDecorationModel( 0, siteDirectory, llocale, project, reactorProjects, localRepository, repositories );
DecorationModel decorationModel = result.getKey();
MavenProject parentProject = result.getValue();
if ( decorationModel == null )
{
getLogger().debug( "Using default site descriptor" );
String siteDescriptorContent;
Reader reader = null;
try
{
// Note the default is not a super class - it is used when nothing else is found
reader = ReaderFactory.newXmlReader( getClass().getResourceAsStream( "/default-site.xml" ) );
siteDescriptorContent = readSiteDescriptor( reader, "default-site.xml" );
}
catch ( IOException e )
{
throw new SiteToolException( "Error reading default site descriptor: " + e.getMessage(), e );
}
finally
{
IOUtil.close( reader );
}
decorationModel = readDecorationModel( siteDescriptorContent );
}
// DecorationModel back to String to interpolate, then go back to DecorationModel
String siteDescriptorContent = decorationModelToString( decorationModel );
// "classical" late interpolation, after full inheritance
siteDescriptorContent = getInterpolatedSiteDescriptorContent( project, siteDescriptorContent, false );
decorationModel = readDecorationModel( siteDescriptorContent );
if ( parentProject != null )
{
populateParentMenu( decorationModel, llocale, project, parentProject, true );
}
populateModulesMenu( decorationModel, llocale, project, reactorProjects, localRepository, true );
if ( decorationModel.getBannerLeft() == null )
{
// extra default to set
Banner banner = new Banner();
banner.setName( project.getName() );
decorationModel.setBannerLeft( banner );
}
return decorationModel;
}
/** {@inheritDoc} */
public String getInterpolatedSiteDescriptorContent( Map<String, String> props, MavenProject aProject,
String siteDescriptorContent )
throws SiteToolException
{
checkNotNull( "props", props );
// "classical" late interpolation
return getInterpolatedSiteDescriptorContent( aProject, siteDescriptorContent, false );
}
private String getInterpolatedSiteDescriptorContent( MavenProject aProject,
String siteDescriptorContent, boolean isEarly )
throws SiteToolException
{
checkNotNull( "aProject", aProject );
checkNotNull( "siteDescriptorContent", siteDescriptorContent );
RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
if ( isEarly )
{
interpolator.addValueSource( new PrefixedObjectValueSource( "this.", aProject ) );
interpolator.addValueSource( new PrefixedPropertiesValueSource( "this.", aProject.getProperties() ) );
}
else
{
interpolator.addValueSource( new ObjectBasedValueSource( aProject ) );
interpolator.addValueSource( new MapBasedValueSource( aProject.getProperties() ) );
try
{
interpolator.addValueSource( new EnvarBasedValueSource() );
}
catch ( IOException e )
{
// Prefer logging?
throw new SiteToolException( "IOException: cannot interpolate environment properties: "
+ e.getMessage(), e );
}
}
try
{
// FIXME: this does not escape xml entities, see MSITE-226, PLXCOMP-118
return interpolator.interpolate( siteDescriptorContent, isEarly ? null : "project" );
}
catch ( InterpolationException e )
{
throw new SiteToolException( "Cannot interpolate site descriptor: " + e.getMessage(), e );
}
}
/** {@inheritDoc} */
public MavenProject getParentProject( MavenProject aProject, List<MavenProject> reactorProjects,
ArtifactRepository localRepository )
{
checkNotNull( "aProject", aProject );
checkNotNull( "reactorProjects", reactorProjects );
checkNotNull( "localRepository", localRepository );
if ( isMaven3OrMore() )
{
// no need to make voodoo with Maven 3: job already done
return aProject.getParent();
}
MavenProject parentProject = null;
MavenProject origParent = aProject.getParent();
if ( origParent != null )
{
for ( MavenProject reactorProject : reactorProjects )
{
if ( reactorProject.getGroupId().equals( origParent.getGroupId() )
&& reactorProject.getArtifactId().equals( origParent.getArtifactId() )
&& reactorProject.getVersion().equals( origParent.getVersion() ) )
{
parentProject = reactorProject;
getLogger().debug( "Parent project " + origParent.getId() + " picked from reactor" );
break;
}
}
if ( parentProject == null && aProject.getBasedir() != null
&& StringUtils.isNotEmpty( aProject.getModel().getParent().getRelativePath() ) )
{
try
{
String relativePath = aProject.getModel().getParent().getRelativePath();
File pomFile = new File( aProject.getBasedir(), relativePath );
if ( pomFile.isDirectory() )
{
pomFile = new File( pomFile, "pom.xml" );
}
pomFile = new File( getNormalizedPath( pomFile.getPath() ) );
if ( pomFile.isFile() )
{
MavenProject mavenProject = mavenProjectBuilder.build( pomFile, localRepository, null );
if ( mavenProject.getGroupId().equals( origParent.getGroupId() )
&& mavenProject.getArtifactId().equals( origParent.getArtifactId() )
&& mavenProject.getVersion().equals( origParent.getVersion() ) )
{
parentProject = mavenProject;
getLogger().debug( "Parent project " + origParent.getId() + " loaded from a relative path: "
+ relativePath );
}
}
}
catch ( ProjectBuildingException e )
{
getLogger().info( "Unable to load parent project " + origParent.getId() + " from a relative path: "
+ e.getMessage() );
}
}
if ( parentProject == null )
{
try
{
parentProject = mavenProjectBuilder.buildFromRepository( aProject.getParentArtifact(), aProject
.getRemoteArtifactRepositories(), localRepository );
getLogger().debug( "Parent project " + origParent.getId() + " loaded from repository" );
}
catch ( ProjectBuildingException e )
{
getLogger().warn( "Unable to load parent project " + origParent.getId() + " from repository: "
+ e.getMessage() );
}
}
if ( parentProject == null )
{
// fallback to original parent, which may contain uninterpolated value (still need a unit test)
parentProject = origParent;
getLogger().debug( "Parent project " + origParent.getId() + " picked from original value" );
}
}
return parentProject;
}
/**
* Populate the pre-defined <code>parent</code> menu of the decoration model,
* if used through <code>&lt;menu ref="parent"/&gt;</code>.
*
* @param decorationModel the Doxia Sitetools DecorationModel, not null.
* @param locale the locale used for the i18n in DecorationModel. If null, using the default locale in the jvm.
* @param project a Maven project, not null.
* @param parentProject a Maven parent project, not null.
* @param keepInheritedRefs used for inherited references.
*/
private void populateParentMenu( DecorationModel decorationModel, Locale locale, MavenProject project,
MavenProject parentProject, boolean keepInheritedRefs )
{
checkNotNull( "decorationModel", decorationModel );
checkNotNull( "project", project );
checkNotNull( "parentProject", parentProject );
Menu menu = decorationModel.getMenuRef( "parent" );
if ( menu == null )
{
return;
}
if ( keepInheritedRefs && menu.isInheritAsRef() )
{
return;
}
final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale;
String parentUrl = getDistMgmntSiteUrl( parentProject );
if ( parentUrl != null )
{
if ( parentUrl.endsWith( "/" ) )
{
parentUrl += "index.html";
}
else
{
parentUrl += "/index.html";
}
parentUrl = getRelativePath( parentUrl, getDistMgmntSiteUrl( project ) );
}
else
{
// parent has no url, assume relative path is given by site structure
File parentBasedir = parentProject.getBasedir();
// First make sure that the parent is available on the file system
if ( parentBasedir != null )
{
// Try to find the relative path to the parent via the file system
String parentPath = parentBasedir.getAbsolutePath();
String projectPath = project.getBasedir().getAbsolutePath();
parentUrl = getRelativePath( parentPath, projectPath ) + "/index.html";
}
}
// Only add the parent menu if we were able to find a URL for it
if ( parentUrl == null )
{
getLogger().warn( "Unable to find a URL to the parent project. The parent menu will NOT be added." );
}
else
{
if ( menu.getName() == null )
{
menu.setName( i18n.getString( "site-tool", llocale, "decorationModel.menu.parentproject" ) );
}
MenuItem item = new MenuItem();
item.setName( parentProject.getName() );
item.setHref( parentUrl );
menu.addItem( item );
}
}
/**
* Populate the pre-defined <code>modules</code> menu of the decoration model,
* if used through <code>&lt;menu ref="modules"/&gt;</code>.
*
* @param decorationModel the Doxia Sitetools DecorationModel, not null.
* @param locale the locale used for the i18n in DecorationModel. If null, using the default locale in the jvm.
* @param project a Maven project, not null.
* @param reactorProjects the Maven reactor projects, not null.
* @param localRepository the Maven local repository, not null.
* @param keepInheritedRefs used for inherited references.
* @throws SiteToolException if any
*/
private void populateModulesMenu( DecorationModel decorationModel, Locale locale, MavenProject project,
List<MavenProject> reactorProjects, ArtifactRepository localRepository,
boolean keepInheritedRefs )
throws SiteToolException
{
checkNotNull( "project", project );
checkNotNull( "reactorProjects", reactorProjects );
checkNotNull( "localRepository", localRepository );
checkNotNull( "decorationModel", decorationModel );
Menu menu = decorationModel.getMenuRef( "modules" );
if ( menu == null )
{
return;
}
if ( keepInheritedRefs && menu.isInheritAsRef() )
{
return;
}
final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale ;
// we require child modules and reactors to process module menu
if ( project.getModules().size() > 0 )
{
if ( menu.getName() == null )
{
menu.setName( i18n.getString( "site-tool", llocale, "decorationModel.menu.projectmodules" ) );
}
getLogger().debug( "Attempting to load module information from local filesystem" );
// Not running reactor - search for the projects manually
List<Model> models = new ArrayList<Model>( project.getModules().size() );
for ( String module : (List<String>) project.getModules() )
{
Model model;
File f = new File( project.getBasedir(), module + "/pom.xml" );
if ( f.exists() )
{
try
{
model = mavenProjectBuilder.build( f, localRepository, null ).getModel();
}
catch ( ProjectBuildingException e )
{
throw new SiteToolException( "Unable to read local module-POM", e );
}
}
else
{
getLogger().warn( "No filesystem module-POM available" );
model = new Model();
model.setName( module );
setDistMgmntSiteUrl( model, module );
}
models.add( model );
}
populateModulesMenuItemsFromModels( project, models, menu );
}
else if ( decorationModel.getMenuRef( "modules" ).getInherit() == null )
{
// only remove if project has no modules AND menu is not inherited, see MSHARED-174
decorationModel.removeMenuRef( "modules" );
}
}
/** {@inheritDoc} */
public void populateReportsMenu( DecorationModel decorationModel, Locale locale,
Map<String, List<MavenReport>> categories )
{
checkNotNull( "decorationModel", decorationModel );
checkNotNull( "categories", categories );
Menu menu = decorationModel.getMenuRef( "reports" );
if ( menu == null )
{
return;
}
final Locale llocale = ( locale == null ) ? Locale.getDefault() : locale;
if ( menu.getName() == null )
{
menu.setName( i18n.getString( "site-tool", llocale, "decorationModel.menu.projectdocumentation" ) );
}
boolean found = false;
if ( menu.getItems().isEmpty() )
{
List<MavenReport> categoryReports = categories.get( MavenReport.CATEGORY_PROJECT_INFORMATION );
if ( !isEmptyList( categoryReports ) )
{
MenuItem item = createCategoryMenu(
i18n.getString( "site-tool", llocale,
"decorationModel.menu.projectinformation" ),
"/project-info.html", categoryReports, llocale );
menu.getItems().add( item );
found = true;
}
categoryReports = categories.get( MavenReport.CATEGORY_PROJECT_REPORTS );
if ( !isEmptyList( categoryReports ) )
{
MenuItem item =
createCategoryMenu( i18n.getString( "site-tool", llocale, "decorationModel.menu.projectreports" ),
"/project-reports.html", categoryReports, llocale );
menu.getItems().add( item );
found = true;
}
}
if ( !found )
{
decorationModel.removeMenuRef( "reports" );
}
}
/** {@inheritDoc} */
public List<Locale> getSiteLocales( String locales )
{
if ( locales == null )
{
return Collections.singletonList( DEFAULT_LOCALE );
}
String[] localesArray = StringUtils.split( locales, "," );
List<Locale> localesList = new ArrayList<Locale>( localesArray.length );
for ( String localeString : localesArray )
{
Locale locale = codeToLocale( localeString );
if ( locale == null )
{
continue;
}
if ( !Arrays.asList( Locale.getAvailableLocales() ).contains( locale ) )
{
if ( getLogger().isWarnEnabled() )
{
getLogger().warn( "The locale defined by '" + locale
+ "' is not available in this Java Virtual Machine ("
+ System.getProperty( "java.version" )
+ " from " + System.getProperty( "java.vendor" ) + ") - IGNORING" );
}
continue;
}
// Default bundles are in English
if ( ( !locale.getLanguage().equals( DEFAULT_LOCALE.getLanguage() ) )
&& ( !i18n.getBundle( "site-tool", locale ).getLocale().getLanguage()
.equals( locale.getLanguage() ) ) )
{
if ( getLogger().isWarnEnabled() )
{
getLogger().warn( "The locale '" + locale + "' (" + locale.getDisplayName( Locale.ENGLISH )
+ ") is not currently supported by Maven Site - IGNORING."
+ "\nContributions are welcome and greatly appreciated!"
+ "\nIf you want to contribute a new translation, please visit "
+ "http://maven.apache.org/plugins/localization.html for detailed instructions." );
}
continue;
}
localesList.add( locale );
}
if ( localesList.isEmpty() )
{
localesList = Collections.singletonList( DEFAULT_LOCALE );
}
return localesList;
}
/**
* Converts a locale code like "en", "en_US" or "en_US_win" to a <code>java.util.Locale</code>
* object.
* <p>If localeCode = <code>default</code>, return the current value of the default locale for this instance
* of the Java Virtual Machine.</p>
*
* @param localeCode the locale code string.
* @return a java.util.Locale object instanced or null if errors occurred
* @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/Locale.html">java.util.Locale#getDefault()</a>
*/
private Locale codeToLocale( String localeCode )
{
if ( localeCode == null )
{
return null;
}
if ( "default".equalsIgnoreCase( localeCode ) )
{
return Locale.getDefault();
}
String language = "";
String country = "";
String variant = "";
StringTokenizer tokenizer = new StringTokenizer( localeCode, "_" );
final int maxTokens = 3;
if ( tokenizer.countTokens() > maxTokens )
{
if ( getLogger().isWarnEnabled() )
{
getLogger().warn( "Invalid java.util.Locale format for '" + localeCode + "' entry - IGNORING" );
}
return null;
}
if ( tokenizer.hasMoreTokens() )
{
language = tokenizer.nextToken();
if ( tokenizer.hasMoreTokens() )
{
country = tokenizer.nextToken();
if ( tokenizer.hasMoreTokens() )
{
variant = tokenizer.nextToken();
}
}
}
return new Locale( language, country, variant );
}
// ----------------------------------------------------------------------
// Protected methods
// ----------------------------------------------------------------------
/**
* @param path could be null.
* @return the path normalized, i.e. by eliminating "/../" and "/./" in the path.
* @see FilenameUtils#normalize(String)
*/
protected static String getNormalizedPath( String path )
{
String normalized = FilenameUtils.normalize( path );
if ( normalized == null )
{
normalized = path;
}
return ( normalized == null ) ? null : normalized.replace( '\\', '/' );
}
// ----------------------------------------------------------------------
// Private methods
// ----------------------------------------------------------------------
/**
* @param project not null
* @param localRepository not null
* @param repositories not null
* @param locale not null
* @return the resolved site descriptor
* @throws IOException if any
* @throws ArtifactResolutionException if any
* @throws ArtifactNotFoundException if any
*/
private File resolveSiteDescriptor( MavenProject project, ArtifactRepository localRepository,
List<ArtifactRepository> repositories, Locale locale )
throws IOException, ArtifactResolutionException, ArtifactNotFoundException
{
File result;
// TODO: this is a bit crude - proper type, or proper handling as metadata rather than an artifact in 2.1?
Artifact artifact = artifactFactory.createArtifactWithClassifier( project.getGroupId(),
project.getArtifactId(),
project.getVersion(), "xml",
"site_" + locale.getLanguage() );
boolean found = false;
try
{
artifactResolver.resolve( artifact, repositories, localRepository );
result = artifact.getFile();
// we use zero length files to avoid re-resolution (see below)
if ( result.length() > 0 )
{
found = true;
}
else
{
getLogger().debug( "No site descriptor found for " + project.getId() + " for locale "
+ locale.getLanguage() );
}
}
catch ( ArtifactNotFoundException e )
{
getLogger().debug( "Unable to locate site descriptor for locale " + locale.getLanguage() + ": " + e );
// we can afford to write an empty descriptor here as we don't expect it to turn up later in the remote
// repository, because the parent was already released (and snapshots are updated automatically if changed)
result = new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) );
result.getParentFile().mkdirs();
result.createNewFile();
}
if ( !found )
{
artifact = artifactFactory.createArtifactWithClassifier( project.getGroupId(), project.getArtifactId(),
project.getVersion(), "xml", "site" );
try
{
artifactResolver.resolve( artifact, repositories, localRepository );
}
catch ( ArtifactNotFoundException e )
{
// see above regarding this zero length file
result = new File( localRepository.getBasedir(), localRepository.pathOf( artifact ) );
result.getParentFile().mkdirs();
result.createNewFile();
throw e;
}
result = artifact.getFile();
// we use zero length files to avoid re-resolution (see below)
if ( result.length() == 0 )
{
getLogger().debug( "No site descriptor found for " + project.getId() + " without locale" );
result = null;
}
}
return result;
}
/**
* @param depth depth of project
* @param siteDirectory, can be null if project.basedir is null, ie POM from repository
* @param locale not null
* @param project not null
* @param reactorProjects not null
* @param localRepository not null
* @param repositories not null
* @param origProps not null
* @return the decoration model depending the locale and the parent project
* @throws SiteToolException if any
*/
private Map.Entry<DecorationModel, MavenProject> getDecorationModel( int depth, File siteDirectory, Locale locale,
MavenProject project,
List<MavenProject> reactorProjects,
ArtifactRepository localRepository,
List<ArtifactRepository> repositories )
throws SiteToolException
{
// 1. get site descriptor File
File siteDescriptor;
if ( project.getBasedir() == null )
{
// POM is in the repository: look into the repository for site descriptor
try
{
siteDescriptor = getSiteDescriptorFromRepository( project, localRepository, repositories, locale );
}
catch ( SiteToolException e )
{
throw new SiteToolException( "The site descriptor cannot be resolved from the repository: "
+ e.getMessage(), e );
}
}
else
{
// POM is in build directory: look for site descriptor as local file
siteDescriptor = getSiteDescriptor( siteDirectory, locale );
}
// 2. read DecorationModel from site descriptor File and do early interpolation (${this.*})
DecorationModel decoration = null;
Reader siteDescriptorReader = null;
try
{
if ( siteDescriptor != null && siteDescriptor.exists() )
{
getLogger().debug( "Reading" + ( depth == 0 ? "" : ( " parent level " + depth ) )
+ " site descriptor from " + siteDescriptor );
siteDescriptorReader = ReaderFactory.newXmlReader( siteDescriptor );
String siteDescriptorContent = readSiteDescriptor( siteDescriptorReader, project.getId() );
// interpolate ${this.*} = early interpolation
siteDescriptorContent = getInterpolatedSiteDescriptorContent( project, siteDescriptorContent, true );
decoration = readDecorationModel( siteDescriptorContent );
decoration.setLastModified( siteDescriptor.lastModified() );
}
else
{
getLogger().debug( "No site descriptor found for " + project.getId() );
}
}
catch ( IOException e )
{
throw new SiteToolException( "The site descriptor for " + project.getId() + " cannot be read from "
+ siteDescriptor, e );
}
finally
{
IOUtil.close( siteDescriptorReader );
}
// 3. look for parent project
MavenProject parentProject = getParentProject( project, reactorProjects, localRepository );
// 4. merge with parent project DecorationModel
if ( parentProject != null && ( decoration == null || decoration.isMergeParent() ) )
{
depth++;
getLogger().debug( "Looking for site descriptor of level " + depth + " parent project: "
+ parentProject.getId() );
File parentSiteDirectory = null;
if ( parentProject.getBasedir() != null )
{
// extrapolate parent project site directory
String siteRelativePath = getRelativeFilePath( project.getBasedir().getAbsolutePath(),
siteDescriptor.getParentFile().getAbsolutePath() );
parentSiteDirectory = new File( parentProject.getBasedir(), siteRelativePath );
// notice: using same siteRelativePath for parent as current project; may be wrong if site plugin
// has different configuration. But this is a rare case (this only has impact if parent is from reactor)
}
DecorationModel parentDecoration =
getDecorationModel( depth, parentSiteDirectory, locale, parentProject, reactorProjects, localRepository,
repositories ).getKey();
// MSHARED-116 requires an empty decoration model (instead of a null one)
// MSHARED-145 requires us to do this only if there is a parent to merge it with
if ( decoration == null && parentDecoration != null )
{
// we have no site descriptor: merge the parent into an empty one
decoration = new DecorationModel();
}
String name = project.getName();
if ( decoration != null && StringUtils.isNotEmpty( decoration.getName() ) )
{
name = decoration.getName();
}
// Merge the parent and child DecorationModels
String projectDistMgmnt = getDistMgmntSiteUrl( project );
String parentDistMgmnt = getDistMgmntSiteUrl( parentProject );
if ( getLogger().isDebugEnabled() )
{
getLogger().debug( "Site decoration model inheritance: assembling child with level " + depth
+ " parent: distributionManagement.site.url child = " + projectDistMgmnt + " and parent = "
+ parentDistMgmnt );
}
assembler.assembleModelInheritance( name, decoration, parentDecoration, projectDistMgmnt,
parentDistMgmnt == null ? projectDistMgmnt : parentDistMgmnt );
}
return new AbstractMap.SimpleEntry<DecorationModel, MavenProject>( decoration, parentProject );
}
/**
* @param siteDescriptorContent not null
* @return the decoration model object
* @throws SiteToolException if any
*/
private DecorationModel readDecorationModel( String siteDescriptorContent )
throws SiteToolException
{
try
{
return new DecorationXpp3Reader().read( new StringReader( siteDescriptorContent ) );
}
catch ( XmlPullParserException e )
{
throw new SiteToolException( "Error parsing site descriptor", e );
}
catch ( IOException e )
{
throw new SiteToolException( "Error reading site descriptor", e );
}
}
private String decorationModelToString( DecorationModel decoration )
throws SiteToolException
{
StringWriter writer = new StringWriter();
try
{
new DecorationXpp3Writer().write( writer, decoration );
return writer.toString();
}
catch ( IOException e )
{
throw new SiteToolException( "Error reading site descriptor", e );
}
finally
{
IOUtil.close( writer );
}
}
private static String buildRelativePath( final String toPath, final String fromPath, final char separatorChar )
{
// use tokenizer to traverse paths and for lazy checking
StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
int count = 0;
// walk along the to path looking for divergence from the from path
while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() )
{
if ( separatorChar == '\\' )
{
if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) )
{
break;
}
}
else
{
if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) )
{
break;
}
}
count++;
}
// reinitialize the tokenizers to count positions to retrieve the
// gobbled token
toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
while ( count-- > 0 )
{
fromTokeniser.nextToken();
toTokeniser.nextToken();
}
StringBuilder relativePath = new StringBuilder();
// add back refs for the rest of from location.
while ( fromTokeniser.hasMoreTokens() )
{
fromTokeniser.nextToken();
relativePath.append( ".." );
if ( fromTokeniser.hasMoreTokens() )
{
relativePath.append( separatorChar );
}
}
if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() )
{
relativePath.append( separatorChar );
}
// add fwd fills for whatever's left of to.
while ( toTokeniser.hasMoreTokens() )
{
relativePath.append( toTokeniser.nextToken() );
if ( toTokeniser.hasMoreTokens() )
{
relativePath.append( separatorChar );
}
}
return relativePath.toString();
}
/**
* @param project not null
* @param models not null
* @param menu not null
*/
private void populateModulesMenuItemsFromModels( MavenProject project, List<Model> models, Menu menu )
{
for ( Model model : models )
{
String reactorUrl = getDistMgmntSiteUrl( model );
String name = ( model.getName() == null ) ? model.getArtifactId() : model.getName();
appendMenuItem( project, menu, name, reactorUrl, model.getArtifactId() );
}
}
/**
* @param project not null
* @param menu not null
* @param name not null
* @param href could be null
* @param defaultHref not null
*/
private void appendMenuItem( MavenProject project, Menu menu, String name, String href, String defaultHref )
{
String selectedHref = href;
if ( selectedHref == null )
{
selectedHref = defaultHref;
}
MenuItem item = new MenuItem();
item.setName( name );
String baseUrl = getDistMgmntSiteUrl( project );
if ( baseUrl != null )
{
selectedHref = getRelativePath( selectedHref, baseUrl );
}
if ( selectedHref.endsWith( "/" ) )
{
item.setHref( selectedHref + "index.html" );
}
else
{
item.setHref( selectedHref + "/index.html" );
}
menu.addItem( item );
}
/**
* @param name not null
* @param href not null
* @param categoryReports not null
* @param locale not null
* @return the menu item object
*/
private MenuItem createCategoryMenu( String name, String href, List<MavenReport> categoryReports, Locale locale )
{
MenuItem item = new MenuItem();
item.setName( name );
item.setCollapse( true );
item.setHref( href );
// MSHARED-172, allow reports to define their order in some other way?
//Collections.sort( categoryReports, new ReportComparator( locale ) );
for ( MavenReport report : categoryReports )
{
MenuItem subitem = new MenuItem();
subitem.setName( report.getName( locale ) );
subitem.setHref( report.getOutputName() + ".html" );
item.getItems().add( subitem );
}
return item;
}
// ----------------------------------------------------------------------
// static methods
// ----------------------------------------------------------------------
/**
* Convenience method.
*
* @param list could be null
* @return true if the list is <code>null</code> or empty
*/
private static boolean isEmptyList( List<?> list )
{
return list == null || list.isEmpty();
}
/**
* Return distributionManagement.site.url if defined, null otherwise.
*
* @param project not null
* @return could be null
*/
private static String getDistMgmntSiteUrl( MavenProject project )
{
return getDistMgmntSiteUrl( project.getDistributionManagement() );
}
/**
* Return distributionManagement.site.url if defined, null otherwise.
*
* @param model not null
* @return could be null
*/
private static String getDistMgmntSiteUrl( Model model )
{
return getDistMgmntSiteUrl( model.getDistributionManagement() );
}
private static String getDistMgmntSiteUrl( DistributionManagement distMgmnt )
{
if ( distMgmnt != null && distMgmnt.getSite() != null && distMgmnt.getSite().getUrl() != null )
{
return urlEncode( distMgmnt.getSite().getUrl() );
}
return null;
}
private static String urlEncode( final String url )
{
if ( url == null )
{
return null;
}
try
{
return new File( url ).toURI().toURL().toExternalForm();
}
catch ( MalformedURLException ex )
{
return url; // this will then throw somewhere else
}
}
private static void setDistMgmntSiteUrl( Model model, String url )
{
if ( model.getDistributionManagement() == null )
{
model.setDistributionManagement( new DistributionManagement() );
}
if ( model.getDistributionManagement().getSite() == null )
{
model.getDistributionManagement().setSite( new Site() );
}
model.getDistributionManagement().getSite().setUrl( url );
}
private void checkNotNull( String name, Object value )
{
if ( value == null )
{
throw new IllegalArgumentException( "The parameter '" + name + "' cannot be null." );
}
}
/**
* Check the current Maven version to see if it's Maven 3.0 or newer.
*/
private static boolean isMaven3OrMore()
{
return new DefaultArtifactVersion( getMavenVersion() ).getMajorVersion() >= 3;
}
private static String getMavenVersion()
{
// This relies on the fact that MavenProject is the in core classloader
// and that the core classloader is for the maven-core artifact
// and that should have a pom.properties file
// if this ever changes, we will have to revisit this code.
final Properties properties = new Properties();
final String corePomProperties = "META-INF/maven/org.apache.maven/maven-core/pom.properties";
final InputStream in = MavenProject.class.getClassLoader().getResourceAsStream( corePomProperties );
try
{
properties.load( in );
}
catch ( IOException ioe )
{
return "";
}
finally
{
IOUtil.close( in );
}
return properties.getProperty( "version" ).trim();
}
}