/*
* Copyright 2004 The Apache Software Foundation.
*
* 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.
*
* $Header:$
*/
package org.apache.beehive.netui.compiler.genmodel;
import org.apache.beehive.netui.compiler.CompilerUtils;
import org.apache.beehive.netui.compiler.FlowControllerInfo;
import org.apache.beehive.netui.compiler.JpfLanguageConstants;
import org.apache.beehive.netui.compiler.MergedControllerAnnotation;
import org.apache.beehive.netui.compiler.Diagnostics;
import org.apache.beehive.netui.compiler.FatalCompileTimeException;
import org.apache.beehive.netui.compiler.model.ActionModel;
import org.apache.beehive.netui.compiler.model.FormBeanModel;
import org.apache.beehive.netui.compiler.model.ForwardModel;
import org.apache.beehive.netui.compiler.model.MessageResourcesModel;
import org.apache.beehive.netui.compiler.model.StrutsApp;
import org.apache.beehive.netui.compiler.typesystem.declaration.AnnotationInstance;
import org.apache.beehive.netui.compiler.typesystem.declaration.ClassDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.MethodDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.Modifier;
import org.apache.beehive.netui.compiler.typesystem.declaration.PackageDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.ParameterDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.TypeDeclaration;
import org.apache.beehive.netui.compiler.typesystem.env.AnnotationProcessorEnvironment;
import org.apache.beehive.netui.compiler.typesystem.type.DeclaredType;
import org.apache.beehive.netui.compiler.typesystem.type.TypeInstance;
import org.apache.beehive.netui.compiler.typesystem.type.ClassType;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
public class GenStrutsApp
extends StrutsApp
implements JpfLanguageConstants
{
private ClassDeclaration _jclass;
private String _containingPackage;
private File _strutsConfigFile;
private File _sourceFile;
private AnnotationProcessorEnvironment _env;
private FlowControllerInfo _fcInfo;
private Diagnostics _diagnostics;
protected void recalculateStrutsConfigFile()
throws XmlException, IOException, FatalCompileTimeException
{
_strutsConfigFile = calculateStrutsConfigFile(); // caching this
}
FlowControllerInfo getFlowControllerInfo()
{
return _fcInfo;
}
public GenStrutsApp( File sourceFile, ClassDeclaration jclass, AnnotationProcessorEnvironment env,
FlowControllerInfo fcInfo, boolean checkOnly, Diagnostics diagnostics )
throws XmlException, IOException, FatalCompileTimeException
{
super( jclass.getQualifiedName() );
_jclass = jclass;
_containingPackage = jclass.getPackage().getQualifiedName();
_sourceFile = sourceFile;
_env = env;
assert fcInfo != null;
_fcInfo = fcInfo;
_diagnostics = diagnostics;
recalculateStrutsConfigFile();
if ( checkOnly ) return;
if ( _jclass != null )
{
MergedControllerAnnotation mca = fcInfo.getMergedControllerAnnotation();
setNestedPageFlow( mca.isNested() );
setLongLivedPageFlow( mca.isLongLived() );
addMessageResources( mca.getMessageResources() ); // messageResources is deprecated
addMessageBundles( mca.getMessageBundles() ); // messageBundles is not
addSimpleActions( mca.getSimpleActions() );
setMultipartHandler( mca.getMultipartHandler() );
GenForwardModel.addForwards( mca.getForwards(), this, _jclass, this, null );
// TODO: comment
addForward( new ForwardModel( "_auto", "", this ) );
GenExceptionModel.addCatches( mca.getCatches(), this, _jclass, this, this );
addTilesDefinitionsConfigs( mca.getTilesDefinitionsConfigs() );
setAdditionalValidatorConfigs( mca.getCustomValidatorConfigs() );
addActionMethods();
addFormBeans( _jclass );
}
if ( fcInfo != null )
{
setSharedFlows( fcInfo.getSharedFlowTypeNames() );
setReturnToActionDisabled( ! fcInfo.isNavigateToActionEnabled() );
setReturnToPageDisabled( ! fcInfo.isNavigateToPageEnabled() );
}
}
private void addFormBeans( ClassDeclaration jclass )
{
Collection innerTypes = CompilerUtils.getClassNestedTypes( jclass );
for ( Iterator ii = innerTypes.iterator(); ii.hasNext(); )
{
TypeDeclaration innerType = ( TypeDeclaration ) ii.next();
if ( innerType instanceof ClassDeclaration )
{
ClassDeclaration innerClass = ( ClassDeclaration ) innerType;
if ( innerType.hasModifier( Modifier.PUBLIC )
&& CompilerUtils.isAssignableFrom( PAGEFLOW_FORM_CLASS_NAME, innerClass, _env ) )
{
addFormBean( innerClass, null );
}
}
}
}
String addFormBean( TypeDeclaration formType, ActionModel usedByAction )
{
String formClass = CompilerUtils.getFormClassName( formType, _env );
//
// Use the actual type of form to create the name.
// This avoids conflicts if there are multiple forms using the
// ANY_FORM_CLASS_NAME type.
//
String actualType = CompilerUtils.getLoadableName( formType );
//
// See if the app already has a form-bean of this type. If so,
// we'll just use it; otherwise, we need to create it.
//
boolean usesPageFlowScopedFormBean = usedByAction != null ? usedByAction.getFormMember() != null : false;
List existingBeans = getFormBeansByActualType( actualType, Boolean.valueOf( usesPageFlowScopedFormBean ) );
String formBeanName;
if ( existingBeans != null )
{
assert existingBeans.size() > 0;
formBeanName = ( ( FormBeanModel ) existingBeans.get( 0 ) ).getName();
}
else
{
formBeanName = getFormNameForType( actualType, usesPageFlowScopedFormBean );
addFormBean( new FormBeanModel( formBeanName, formClass, actualType, usesPageFlowScopedFormBean, this ) );
getMessageResourcesFromForm( formType, usedByAction );
}
return formBeanName;
}
private void addMessageResources( Collection messageResources )
{
if ( messageResources != null )
{
for ( Iterator ii = messageResources.iterator(); ii.hasNext(); )
{
AnnotationInstance ann = ( AnnotationInstance ) ii.next();
addMessageResources( new GenMessageBundleModel( this, ann ) );
}
}
}
private void addMessageBundles( Collection messageBundles )
{
if ( messageBundles != null )
{
for ( Iterator ii = messageBundles.iterator(); ii.hasNext(); )
{
AnnotationInstance ann = ( AnnotationInstance ) ii.next();
addMessageResources( new GenMessageBundleModel( this, ann ) );
}
}
}
private void addSimpleActions( Collection simpleActionAnnotations )
{
if ( simpleActionAnnotations != null )
{
for ( Iterator ii = simpleActionAnnotations.iterator(); ii.hasNext(); )
{
AnnotationInstance ann = ( AnnotationInstance ) ii.next();
addActionMapping( new GenSimpleActionModel( ann, this, _jclass ) );
}
}
}
private void setMultipartHandler( String mpHandler )
{
if ( mpHandler != null )
{
if ( mpHandler.equals( MULTIPART_HANDLER_MEMORY_STR ) )
{
setMultipartHandlerClassName( MULTIPART_HANDLER_MEMORY_CLASSNAME );
}
else if ( mpHandler.equals( MULTIPART_HANDLER_DISK_STR ) )
{
setMultipartHandlerClassName( MULTIPART_HANDLER_DISK_CLASSNAME );
}
else
{
assert mpHandler.equals( MULTIPART_HANDLER_DISABLED_STR );
setMultipartHandlerClassName( "none" );
}
}
}
private void addTilesDefinitionsConfigs( List tilesDefinitionsConfigs )
{
if ( tilesDefinitionsConfigs == null || tilesDefinitionsConfigs.isEmpty() )
{
return;
}
List paths = new ArrayList();
for ( Iterator ii = tilesDefinitionsConfigs.iterator(); ii.hasNext(); )
{
String definitionsConfig = ( String ) ii.next();
if ( definitionsConfig != null && definitionsConfig.length() > 0 )
{
paths.add( definitionsConfig );
}
}
setTilesDefinitionsConfigs( paths );
}
private void addActionMethods()
{
MethodDeclaration[] actionMethods = CompilerUtils.getClassMethods( _jclass, ACTION_TAG_NAME );
for ( int i = 0; i < actionMethods.length; i++ )
{
MethodDeclaration actionMethod = actionMethods[i];
if ( ! actionMethod.hasModifier( Modifier.ABSTRACT ) )
{
ActionModel actionModel = new GenActionModel( actionMethod, this, _jclass );
addActionMapping( actionModel );
ParameterDeclaration[] params = actionMethod.getParameters();
if ( params.length > 0 )
{
ParameterDeclaration param1 = params[0];
TypeInstance paramType = param1.getType();
if ( paramType instanceof DeclaredType )
{
getMessageResourcesFromForm( CompilerUtils.getDeclaration( ( DeclaredType ) paramType ), actionModel );
}
}
}
}
}
private void getMessageResourcesFromForm( TypeDeclaration formTypeDecl, ActionModel actionModel )
{
if ( ! ( formTypeDecl instanceof ClassDeclaration ) ) return;
ClassDeclaration formClassDecl = ( ClassDeclaration ) formTypeDecl;
while ( true )
{
AnnotationInstance ann = CompilerUtils.getAnnotation( formClassDecl, FORM_BEAN_TAG_NAME );
if ( ann != null )
{
String defaultMessageResources = CompilerUtils.getString( ann, MESSAGE_BUNDLE_ATTR, true );
if ( defaultMessageResources != null )
{
for ( Iterator ii = getMessageResourcesList().iterator(); ii.hasNext(); )
{
MessageResourcesModel i = ( MessageResourcesModel ) ii.next();
if ( i.getParameter().equals( defaultMessageResources ) ) return;
}
MessageResourcesModel mrm = new MessageResourcesModel( this );
String key = "formMessages:" + CompilerUtils.getLoadableName( formClassDecl );
mrm.setKey( key );
mrm.setParameter( defaultMessageResources );
mrm.setReturnNull( true );
addMessageResources( mrm );
if ( actionModel != null ) actionModel.setFormBeanMessageResourcesKey( key );
}
}
ClassType superType = formClassDecl.getSuperclass();
if ( superType == null ) break;
formClassDecl = superType.getClassTypeDeclaration();
}
}
protected String getMergeFileName()
{
return getFlowControllerInfo().getMergedControllerAnnotation().getStrutsMerge();
}
public void writeToFile()
throws FileNotFoundException, IOException, XmlException, FatalCompileTimeException
{
writeToFile( getMergeFile( getMergeFileName() ) );
}
public boolean isStale()
throws FatalCompileTimeException
{
return isStale( getMergeFile( getMergeFileName() ) );
}
protected boolean isModuleDeclaredInWebXml()
{
// Only the root page flow (which generates a module for path "/") is declared in web.xml
PackageDeclaration pkg = _jclass.getPackage();
return ! isSharedFlow() && pkg == null || pkg.getQualifiedName().length() == 0;
}
String getOutputFileURI( String filePrefix )
{
return getOutputFileURI( filePrefix, _containingPackage, false );
}
String getStrutsConfigURI()
{
return getStrutsConfigURI( _containingPackage, false );
}
protected String getContainingPackage()
{
return _containingPackage;
}
private File calculateStrutsConfigFile()
throws XmlException, IOException, FatalCompileTimeException
{
String webappBuildRoot = CompilerUtils.getWebBuildRoot( getEnv() );
File strutsConfigFile = new File( webappBuildRoot + getStrutsConfigURI() );
//
// For the root Controller.jpf and for Global.app, we have to look in web.xml to get the output location.
// See the comment on getAlternateLocation for a rationale...
//
if ( isModuleDeclaredInWebXml() )
{
String alternateLocation = getAlternateLocation( strutsConfigFile );
if ( alternateLocation != null ) return new File( webappBuildRoot + alternateLocation );
}
return strutsConfigFile;
}
/**
* Tell whether the struts output file (jpf-struts-config-*.xml) is out of date, based on the
* file times of the source file and the (optional) struts-merge file.
*/
public boolean isStale( File mergeFile )
{
//
// We always write the root-level JPF and Global.app, because the struts XML
// config files for these modules are provided by default, and may be out of
// date, even if the file modification times don't indicate that this is true.
//
if ( isModuleDeclaredInWebXml() )
{
return true;
}
//
// We can write to the file if it doesn't exist yet.
//
if ( ! _strutsConfigFile.exists() )
{
return true;
}
long lastWrite = _strutsConfigFile.lastModified();
if ( mergeFile != null && mergeFile.exists() && mergeFile.lastModified() > lastWrite )
{
return true;
}
if ( _sourceFile.lastModified() > lastWrite )
{
return true;
}
return false;
}
/**
* In some cases, canWrite() does not guarantee that a FileNotFoundException will not
* be thrown when trying to write to a file. This method actually tries to overwrite
* the file as a test to see whether it's possible.
*/
public boolean canWrite()
{
if ( ! _strutsConfigFile.canWrite() )
{
return false;
}
try
{
//
// This appears to be the only way to predict whether the file can actually be
// written to; it may be that canWrite() returns true, but the file permissions
// (NTFS only?) will cause an exception to be thrown.
//
new FileOutputStream( _strutsConfigFile, true ).close();
}
catch ( FileNotFoundException e )
{
return false;
}
catch ( IOException e )
{
return false;
}
return true;
}
public void writeToFile( File strutsMergeFile )
throws FileNotFoundException, IOException, XmlException, FatalCompileTimeException
{
_strutsConfigFile.getParentFile().mkdirs();
PrintStream out = new PrintStream( new FileOutputStream( _strutsConfigFile ) );
writeXml( out, strutsMergeFile, CompilerUtils.getWebBuildRoot( getEnv() ) );
out.close();
}
public File getStrutsConfigFile()
{
return _strutsConfigFile;
}
private static boolean isAtElement( XmlCursor curs, String localName )
{
return curs.getName().getLocalPart().equals( localName );
}
/**
* Two special files, the module configs for the root module and "-global", are registered in
* web.xml explicitly. If the user is pointing to an alternate (e.g., old) location for these
* files, we need to compile to that location.
*/
private String getAlternateLocation( File strutsConfigFile )
throws XmlException, IOException, FatalCompileTimeException
{
String webappContentRoot = CompilerUtils.getWebContentRoot( getEnv() );
File webXmlFile = new File( webappContentRoot + '/' + StrutsApp.WEBINF_DIR_NAME + "/web.xml" );
if ( ! webXmlFile.canRead() )
{
_diagnostics.addWarning( _jclass, "warning.could-not-read-web-xml", webappContentRoot );
return null;
}
String strutsConfigFileName = strutsConfigFile.getName();
//
// We're going to parse web.xml in a "loose" way, so we can accept both the Servlet 2.3 and Servlet 2.4 versions
// (and beyond) of the schema. We're looking for this fragment:
//
// <servlet-name>action</servlet-name>
// <init-param>
// <param-name>config</param-name>
// <param-value>/WEB-INF/.pageflow-struts-generated/jpf-struts-config.xml</param-value>
// </init-param>
// ...
//
// If this refers to the basename of our generated struts-config file, we'll use it to determine (override) the
// location of the output file.
//
XmlObject webXmlDoc = XmlObject.Factory.parse( webXmlFile );
XmlCursor curs = webXmlDoc.newCursor();
if ( curs.toFirstChild() && curs.toFirstChild() )
{
do
{
if ( isAtElement( curs, "servlet" ) )
{
XmlCursor i = curs.newCursor();
i.toFirstChild();
do
{
if ( isAtElement( i, "servlet-name" ) && i.getTextValue().equals( "action" ) )
{
XmlCursor j = curs.newCursor();
j.toFirstChild();
do
{
if ( isAtElement( j, "init-param" ) )
{
XmlCursor k = j.newCursor();
k.toFirstChild();
boolean isConfig = false;
String alternateLocation = null;
do
{
if ( isAtElement( k, "param-name" ) && k.getTextValue().startsWith( "config" ) )
{
isConfig = true;
}
else if ( isAtElement( k, "param-value" ) )
{
alternateLocation =
parseAlternateLocation( k.getTextValue(), strutsConfigFileName );
}
} while ( k.toNextSibling() );
if ( isConfig && alternateLocation != null )
{
return alternateLocation;
}
}
} while ( j.toNextSibling() );
//
// We found the action servlet, but no init-param gave an alternate location for our
// struts-config output file.
//
return null;
}
} while ( i.toNextSibling() );
}
} while ( curs.toNextSibling() );
}
return null;
}
private static String parseAlternateLocation( String paramValue, String strutsConfigFileName )
{
//
// If the referenced struts-config file has the same name as the file
// we're going to generate, use the referenced file (its location may be
// different than our default location).
//
if ( paramValue.indexOf( strutsConfigFileName ) != -1 )
{
//
// This may be a comma-separated list of files. Find the right one.
//
if ( paramValue.indexOf( "," ) != -1 )
{
String[] files = paramValue.split( "," );
for ( int k = 0; k < files.length; ++k )
{
if ( files[k].indexOf( strutsConfigFileName ) != -1 )
{
return files[k].trim();
}
}
}
else
{
return paramValue;
}
}
return null;
}
public File getMergeFile( String mergeFileName )
throws FatalCompileTimeException
{
if ( mergeFileName != null )
{
return CompilerUtils.getFileRelativeToSourceFile( _jclass, mergeFileName, getEnv() );
}
return null;
}
protected String getHeaderComment( File mergeFile )
throws FatalCompileTimeException
{
StringBuffer comment = new StringBuffer( " Generated from " );
comment.append( getWebappRelativePath( _sourceFile ) );
if ( mergeFile != null )
{
comment.append( " and " ).append( getWebappRelativePath( mergeFile ) );
}
comment.append( " on " ).append( new Date().toString() ).append( ' ' );
return comment.toString();
}
private String getWebappRelativePath( File file )
throws FatalCompileTimeException
{
String filePath = file.getAbsoluteFile().getPath();
String[] sourceRoots = CompilerUtils.getWebSourceRoots( _env );
//
// Look through the source roots.
//
for ( int i = 0; i < sourceRoots.length; i++ )
{
String sourceRoot = sourceRoots[i];
if ( filePath.startsWith( sourceRoot ) )
{
return file.toString().substring( sourceRoot.length() ).replace( '\\', '/' );
}
}
//
// Look in the web content root.
//
String webContentRoot = CompilerUtils.getWebContentRoot( getEnv() );
if ( filePath.startsWith( webContentRoot ) )
{
return file.toString().substring( webContentRoot.length() ).replace( '\\', '/' );
}
assert false : "could not calculate webapp-relative file from " + file;
return file.toString();
}
AnnotationProcessorEnvironment getEnv()
{
return _env;
}
}