1 /*
2  * Licensed under the Apache License, Version 2.0 (the "License");
3  * you may not use this file except in compliance with the License.
4  * You may obtain a copy of the License at
5  *
6  * http://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software
9  * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limiations under the License.
13 */
14
15package org.tp23.antinstaller.runtime.exe;
16
17import java.io.File;
18import java.io.FileInputStream;
19import java.io.FileNotFoundException;
20import java.io.IOException;
21import java.util.ArrayList;
22import java.util.Collections;
23import java.util.Iterator;
24import java.util.List;
25import java.util.Properties;
26import java.util.ResourceBundle;
27import java.util.StringTokenizer;
28
29import org.tp23.antinstaller.InstallException;
30import org.tp23.antinstaller.Installer;
31import org.tp23.antinstaller.InstallerContext;
32import org.tp23.antinstaller.PropertiesFileRenderer;
33import org.tp23.antinstaller.input.ConditionalField;
34import org.tp23.antinstaller.input.InputField;
35import org.tp23.antinstaller.input.OutputField;
36import org.tp23.antinstaller.input.PasswordTextInput;
37import org.tp23.antinstaller.input.TargetInput;
38import org.tp23.antinstaller.input.TargetSelectInput;
39import org.tp23.antinstaller.page.Page;
40import org.tp23.antinstaller.runtime.VersionHelper;
41import org.tp23.antinstaller.runtime.exe.ExecuteRunnerFilter.AbortException;
42
43
44/**
45 * Loads properties from a file of default properties if found.
46 * the Installer element should define an attribute
47 * loadDefaults with one of the following values.
48 * <li>false - do not look for defaults</li>
49 * <li>prompt - look for properties and ask if they should be used if found</li>
50 * <li>load - look for defaults if found load them</li>
51 * <li>prompt-auto - wierd case where installer permits zero user interaction running only from antinstaller-config.xml defaults</li>
52 * 
53 * N.B. this is not a generic property loader but one specifically for properties files
54 * generated by a previous run of an identical installer or one that according to the version
55 * number is compatible, see PropertyTask for loading other property sets
56 * FIXME i18n for AbortExceptoins
57 * @author teknopaul
58 *
59 */
60public class PropertyLoaderFilter implements ExecuteFilter {
61    
62    private static final ResourceBundle res = ResourceBundle.getBundle("org.tp23.antinstaller.renderer.Res");
63    
64    public static final String LOAD = "true";
65    public static final String PROMPT = "prompt";
66    public static final String PROMPT_AUTO = "prompt-auto";
67    public static final String FALSE = "false";
68    public static final String DEFAULT_PROPERTIES_FILE_PROPERTY = "antinstaller.properties";
69
70    private final String fileNameProperty;
71
72    private int definedPropertiesCount;
73
74    /**
75     * Default constructor required for an ExecuteFilter implementation.
76     * The default property name given by @see{DEFAULT_PROPERTIES_FILE_PROPERTY}
77     * is used with this constructor
78     */
79    public PropertyLoaderFilter() {
80        this( DEFAULT_PROPERTIES_FILE_PROPERTY );
81    }
82
83    /**
84     * Constructor that allows the name of the property containg the properties file
85     * to be specified
86     *
87     * @param fileNameProperty property containing the name of file
88     */
89    public PropertyLoaderFilter( final String fileNameProperty ) {
90        this.fileNameProperty = fileNameProperty;
91    }
92
93    /**
94     * Execute the filter action - in this case pre-setting InputField values
95     * with values loaded from a properties file (if present)
96     *
97     * @see org.tp23.antinstaller.runtime.exe.ExecuteFilter
98     * @param ctx context data
99     * @throws InstallException if an error occurred loading pre-defined properties
00     */
01    public void exec(InstallerContext ctx) throws InstallException {
02
03        Installer installer = ctx.getInstaller();
04        String loadDefaults = installer.getLoadDefaults();
05        if(installer.isVerbose()) {
06            ctx.log("loadDefaults attribute:" + loadDefaults);
07        }
08        boolean load = false;
09        if(loadDefaults == null || FALSE.equals(loadDefaults)) {
10            if(installer.isVerbose()) {
11                ctx.log("Not loading defaults");
12            }
13            return;
14        }
15        
16        ctx.log( "Checking for predefined properties");
17        Properties predefinedProps = loadPredefinedProperties( ctx, fileNameProperty );
18
19        definedPropertiesCount = predefinedProps.size();
20
21        boolean foundProps = false;
22        if( definedPropertiesCount == 0 ) {
23            ctx.log( "No predefined properties");
24        }
25        else{
26            foundProps = true;
27        }
28
29        if( foundProps && PROMPT.equals(loadDefaults) ) {
30            load = ctx.getMessageRenderer().prompt(res.getString("promptLoadDefaults"));
31        }
32        else if( foundProps && PROMPT_AUTO.equals(loadDefaults)) {
33            load = ctx.getMessageRenderer().prompt(res.getString("promptLoadDefaults"));
34        }
35        else if( foundProps && LOAD.equals(loadDefaults) ) {
36            load = true;
37        }
38        
39        if( (!foundProps || !load) && 
40                ctx.isAutoBuild() && 
41                PROMPT.equals(loadDefaults) ) {
42            ctx.log( "Cant run -auto install without properties");
43            throw new AbortException("Install Aborted: cant load ant.install.properties");
44        }
45
46        if(load) {
47            if(installer.isVerbose()) {
48                ctx.log("Loading defaults");
49            }
50            
51            // version control
52            String propertiesVersion = predefinedProps.getProperty(PropertiesFileRenderer.INSTALLER_VERSION_PROPERTY);
53            String configVersion = ctx.getInstaller().getVersion();
54            if(propertiesVersion != null) {
55                VersionHelper helper = new VersionHelper();
56                if( ( ! propertiesVersion.equals(configVersion)) && 
57                        helper.equalOrHigher(configVersion , propertiesVersion) ) {
58                    
59                    // let major versions pass but prompt for differences
60                    if( (! ctx.isAutoBuild()) && helper.majorVersionCompatible(configVersion , propertiesVersion) ){
61                        if( ! ctx.getMessageRenderer().prompt(res.getString("propertiesVersionMismatch")) ){
62                            throw new AbortException("Install Aborted: existing configuration is not compatible, config version: " + configVersion);
63                        }
64                    }
65                    else {
66                        throw new AbortException("Install Aborted: existing configuration is not compatible, config version: " + configVersion);
67                    }
68                }
69                
70            }
71            else {
72                throw new AbortException("Install Aborted: local ant.install.properties missing config version, must be equal or lower than: " + configVersion);
73            }
74            // end version control
75
76            Page[] allPages = installer.getPages();
77            handleDefaults( ctx, allPages, predefinedProps );
78
79        }
80    }
81
82    /*
83     * Use the supplied properties to pre-populate the page fields
84     */
85    private void handleDefaults( InstallerContext ctx, Page[] allPages, Properties props ) throws InstallException {
86        for( int i = 0; i < allPages.length; i++ ) {
87            OutputField[] fields = allPages[i].getOutputField();
88            setInputValues( ctx, allPages[i], fields, props );
89        }
90    }
91
92    private void setInputValues( InstallerContext ctx, Page page, OutputField[] outputFields, Properties props )
93            throws InstallException {
94        //Should never happen, but guard against it
95        if( outputFields == null ) {
96            return;
97        }
98
99        // find relevant targets
00        String targets = props.getProperty(page.getName() + PropertiesFileRenderer.TARGETS_SUFFIX);
01        List targetsList = splitTargets(targets);
02
03        for (int j = 0; j < outputFields.length; j++) {
04            OutputField field = outputFields[j];
05
06            if( field instanceof ConditionalField )  {
07                ConditionalField condField = (ConditionalField) field;
08                setInputValues( ctx, page, condField.getFields(), props );
09            }
10            else if( field instanceof InputField ) {
11                InputField input = (InputField)field;
12                String propName = input.getProperty();
13                if( props.containsKey( propName ) ) {
14                    String value = props.getProperty(propName);
15
16                    if( ctx.getInstaller().isDebug() ) {
17                           ctx.log( "Setting " + propName + "=" + value );
18                    }
19
20                    input.setDefaultValue(value); // does not evaluate references
21                    input.setInputResult(value);
22                    input.setEditted( true );
23
24                    if(field instanceof PasswordTextInput) {
25                        if(value == null ){
26                            ctx.getMessageRenderer().printMessage(res.getString("promptMissingDefaultPassword"));
27
28                        }
29                    }
30
31                    // TARGET TYPES
32                    if(field instanceof TargetInput) {
33                        // Target and SelectTarget
34                        TargetInput tgtInput = (TargetInput)field;
35                        page.removeTarget(tgtInput.getIdx());
36                        // if target was selected
37                        if( ! InputField.isFalse(value)) {
38                            page.addTarget(tgtInput.getIdx(), tgtInput.getTarget()); // returns the OS specific suffix if relevant
39                            // DEBUG
40                            if( ! targetsList.contains(tgtInput.getTarget()) ){ 
41                                // could be caused by someone trying to copy a file across platforms (not a good idea)
42                                ctx.log("Defaults error: targets list for page " + page.getName()
43                                        + " should contain a TargetInput that was true");
44                            }
45                        }
46                        else {
47                            if(InputField.isTrue( tgtInput.getForce()) ) {
48                                String msg = "Defaults error: forced target for page " + page.getName()
49                                + " has been removed";
50                                ctx.log(msg);
51                                throw new InstallException(msg);
52                            }
53                        }
54                    }
55                    if(field instanceof TargetSelectInput) {
56                        TargetSelectInput tgtInput = (TargetSelectInput)field;
57                        page.removeTarget(tgtInput.getIdx());
58                        // one target must be selected (what if the page was not shown??)
59                        page.addTarget(tgtInput.getIdx(), value);
60                    }
61                }
62            }
63            //TODO: Should properties that are present in properties file but which do not appear
64            //      as an InputField be set in the ResultContainer so that they can be used by later
65            //      "if" conditions? - no other properties should be loaded separately from additional
66            //      resource files if there is a requirement for that using an postDisplayTarget - PH
67        }
68
69        //Page targets should be handled by the config loader process and indexed correctly
70        List pageTargets = page.getTargets(ctx);
71        Iterator iter = targetsList.iterator();
72        while (iter.hasNext()) {
73            String targetPerProps = (String) iter.next();
74            if( ! pageTargets.contains(targetPerProps)) {
75                ctx.log("Defaults warning: targets list for page " + page.getName()
76                        + " should contain " + targetPerProps);
77            }
78        }
79
80    }
81
82    /**
83     * Check if external properties have been loaded
84     *
85     * @return <code>true</code> if an external properties file was configured and contained
86     *       at least one property
87     */
88    protected boolean isPropertiesLoaded() {
89        return (definedPropertiesCount > 0);
90    }
91
92    /*
93     * Primarily for unit testing
94     */
95    int getPropertiesFoundCount() {
96        return definedPropertiesCount;
97    }
98
99    /**
00     * Load properties from a properties file if present.
01     * The name of the properties file is checked for in the following order.
02     * <p>
03     * If the parameter fileNamePropertyName is not null:
04     * <ul>
05     * <li>the environment is checked for an environmentvariable with that name</li>
06     * <li>java system properties are checked for a property with that name</li>
07     * </ul>
08     * If the file name has not been found, or if <code>fileNamePropertyName == null</code>
09     * then the default file name is used - @see{org.tp23.antinstaller.PropertiesFileRenderer#PROPERTIES_FILE_NAME}
10     *
11     * @param context installer context
12     * @param fileNamePropertyName name of environment variable or java system property containing the
13     *                             name of the properties file to be loaded or <code>null</code>
14     * @return properties
15     * @throws InstallException if the properties file is missing or an error occurs loading it
16     */
17    private Properties loadPredefinedProperties( final InstallerContext context,
18                                                 final String           fileNamePropertyName )
19            throws InstallException {
20
21        Properties contextProps = InstallerContext.getEnvironment();
22        String propertiesFileName = null;
23        boolean failSilently = true;
24
25        if( fileNamePropertyName != null ) {
26            propertiesFileName = contextProps.getProperty( InstallerContext.ENV_PREFIX + fileNamePropertyName );
27
28            if( propertiesFileName == null ) {
29                propertiesFileName =
30                        contextProps.getProperty( InstallerContext.JAVA_PREFIX + fileNamePropertyName );
31            }
32
33            if( propertiesFileName != null ) {
34                //Properties have been passed explicitly to installer so must load them
35                failSilently = false;
36            }
37        }
38
39        if( propertiesFileName == null ) {
40            propertiesFileName = PropertiesFileRenderer.PROPERTIES_FILE_NAME;
41        }
42
43        Properties definedProperties = new Properties( );
44
45        if( propertiesFileName != null ) {
46            File definedPropertiesFile = new File( propertiesFileName );
47            context.log( "Loading pre-defined properties from file "
48                           + definedPropertiesFile.getAbsolutePath());
49
50            //TODO: Support loading properties file from via classloader as a resource
51            try {
52                FileInputStream istream = new FileInputStream( definedPropertiesFile );
53                definedProperties.load( istream );
54                istream.close();
55            }
56            catch( FileNotFoundException fnfExc ) {
57                if( !failSilently ) {
58                    throw new InstallException( "Defined properties file "
59                                                + definedPropertiesFile.getAbsolutePath()
60                                                + " doesn't exist" );
61                }
62            }
63            catch( IOException ioExc ) {
64                if( !failSilently ) {
65                    throw new InstallException( "Unable to read contents of defined properties file "
66                                                 + definedPropertiesFile.getAbsolutePath(),
67                                                ioExc );
68                }
69            }
70
71            if( context.getInstaller().isDebug() ) {
72                logPropertiesLoaded( context, definedProperties, definedPropertiesFile );
73            }
74
75        }
76
77        return definedProperties;
78    }
79
80
81    // Debug - log properties loaded
82    private void logPropertiesLoaded( final InstallerContext context,
83                                      final Properties properties,
84                                      final File propertiesFile ) {
85        Iterator iterator = properties.keySet().iterator();
86        context.log( "Predefined properties ("
87                        + definedPropertiesCount
88                        + ") loaded from "
89                        + propertiesFile.getAbsolutePath()
90                        + "..." );
91        while( iterator.hasNext() ) {
92            String key = (String) iterator.next();
93            context.log( key + "=" + properties.getProperty( key ) );
94        }
95    }
96
97    /*
98     * Could do a String.split(",") but want to avoid 1.4 specific stuff generally
99     * @param commaSeparated
00     * @return
01     */
02    private List splitTargets(String commaSeparated) {
03        if(commaSeparated == null) {
04            return Collections.EMPTY_LIST;
05        }
06        StringTokenizer st = new StringTokenizer(commaSeparated, ",");
07        List targets = new ArrayList();
08        while (st.hasMoreElements()) {
09            String element = st.nextToken();
10            if(element != null){
11                element = element.trim();
12                if(element.length() > 0){
13                    targets.add(element.trim());
14                }
15            }
16        }
17        return targets;
18    }
19}
20