001/*
002 * Copyright (c) 2003-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2012-06-14 16:45:38 +0000 (Thu, 14 Jun 2012) $' 
007 * '$Revision: 29944 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.kepler.configuration;
031
032import java.io.File;
033import java.io.FileInputStream;
034import java.util.ArrayList;
035import java.util.HashMap;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Locale;
039import java.util.Map;
040import java.util.Vector;
041
042import org.apache.commons.configuration.ConfigurationException;
043import org.apache.commons.configuration.XMLConfiguration;
044import org.apache.commons.configuration.tree.ConfigurationNode;
045import org.apache.commons.logging.Log;
046import org.apache.commons.logging.LogFactory;
047import org.kepler.build.modules.Module;
048import org.kepler.build.modules.ModuleTree;
049import org.kepler.util.DotKeplerManager;
050
051/**
052 * An interface to the deserialization methods required by the configuration
053 * manager to deserialize properties
054 */
055public class CommonsConfigurationReader implements ConfigurationReader
056{
057  private ConfigurationWriter configurationWriter;
058  
059  /** Logging. */
060  private final static Log _log = LogFactory.getLog(CommonsConfigurationReader.class);
061  
062  /** True if log level is set to DEBUG. */
063  private final static boolean _isDebugging = _log.isDebugEnabled();
064
065  /**
066   * constructor
067   */
068  public CommonsConfigurationReader()
069  {
070  }
071
072  /**
073   * constructor
074   */
075  public CommonsConfigurationReader(ConfigurationWriter configurationWriter)
076  {
077      this.configurationWriter = configurationWriter;
078  }
079  
080  /**
081   * load all configurations for a given module
082   */
083  public List<RootConfigurationProperty> loadConfigurations(Module m)
084    throws ConfigurationManagerException
085  {
086    // NOTE: the following are for testing. Oracle JDK appears to
087    // ignore the value of $LANG for Locale.getDefault().
088    //Locale l = new Locale("en" ,"US");
089    //Locale l = new Locale("fi" ,"FI");
090    //Locale l = new Locale("zh", "TW");
091    //return loadConfigurations(m, l); //Locale.getDefault());
092    return loadConfigurations(m, Locale.getDefault());
093  }
094  
095  /**
096   * load all configurations for a given module
097   */
098  public List<RootConfigurationProperty> loadConfigurations(Module m, Locale l)
099    throws ConfigurationManagerException
100  {
101    Vector<RootConfigurationProperty> configs = new Vector<RootConfigurationProperty>();
102    File configDir = m.getConfigurationsDir();
103    if(configDir == null || !configDir.exists())
104    {
105      return null;
106    }
107    
108    try
109    {
110      File[] configFiles = configDir.listFiles();
111      //set the name of this config prop as the name of the config file
112      for(File f : configFiles)
113      {
114        RootConfigurationProperty cp = null;
115        if(!f.isDirectory())
116        { //try to open each file with the commons reader
117          if(f.getName().endsWith(".xml"))
118          { //use the xml reader
119            //System.out.println("loading config file " + f.getName() + " for module " + m.getName());
120            cp = loadConfiguration(m, f, l);
121            //if(cp == null) { System.out.println("is NULL"); }
122          }
123        }
124        
125        if(cp != null)
126        {
127          configs.add(cp);
128        }
129      }
130    }
131    catch(Exception e)
132    {
133      e.printStackTrace();
134      throw new ConfigurationManagerException(
135        "Error loading configuration: " + e.getMessage());
136    }
137        
138    return configs;
139  }
140  
141  /**
142   * load the configuration for a single file in a module
143   */
144  public RootConfigurationProperty loadConfiguration(Module m, File f)
145    throws ConfigurationManagerException
146  {
147    return loadConfiguration(m, f, Locale.getDefault());
148  }
149  
150  /**
151   * load the configuration for a single file in a module
152   */
153  public RootConfigurationProperty loadConfiguration(Module m, File f, Locale l)
154    throws ConfigurationManagerException
155  {
156    try
157    {
158      boolean loadfile = loadThisFile(f, l);
159      //System.out.println("Should we load " + f.getName() +
160        //" for locale " + l.toString() + " : " + loadfile);
161      if(!loadfile)
162      { //don't load this file if it's not of the correct locale
163        return null;
164      }
165
166
167      f = ConfigurationManager.getOverwriteFile(m, f);
168      
169      XMLConfiguration xmlconfig = new XMLConfiguration();
170      xmlconfig.setDelimiterParsingDisabled(true);
171      
172      FileInputStream stream = null;
173      try
174      {
175          stream = new FileInputStream(f);
176          xmlconfig.load(stream);
177      }
178      finally
179      {
180          if(stream != null)
181          {
182              stream.close();
183          }
184      }
185
186      //David Welker: Adding configuration directives
187      boolean mustSave = applyConfigurationDirectives(m, f, xmlconfig);
188
189      RootConfigurationProperty cp = new RootConfigurationProperty(m, f);
190      //set the default namespace
191      ConfigurationNamespace namespace = new ConfigurationNamespace(
192        ConfigurationManager.removeLocaleDesignator(
193          ConfigurationManager.removeFileExtension(f.getName())));
194      cp.setNamespace(namespace);
195      
196      ConfigurationNode rootCN = xmlconfig.getRootNode();
197      
198      //check to see if there is a namsepace element and use that if there is
199      if(rootCN.getChild(0).getName().equals("namespace"))
200      { 
201        String ns = (String)rootCN.getChild(0).getValue();
202        namespace = new ConfigurationNamespace(ns);
203        cp.setNamespace(namespace);
204      }
205      //create the config property
206      ConfigurationProperty deserializedProp = getConfiguration(rootCN, 
207        m, namespace);
208      
209      cp.addProperty(deserializedProp);
210
211      if( mustSave && configurationWriter != null)
212        configurationWriter.writeConfiguration(cp);
213
214      return cp;
215    }
216    catch(Exception e)
217    {
218      e.printStackTrace();
219      throw new ConfigurationManagerException("Error loading configuration file " +
220        f.getPath() + ": " + e.getMessage());
221    }
222  }
223
224    /**
225     * Author: David Welker
226     *
227     * This method will apply any previously unrun configuration directives to the configuration.
228     * Note: Remove configuration and change configuration do not do anything yet, pending further discussion.
229     * @param xmlconfig
230     * @return Returns true if a configuration directive is applied.
231     */
232    private boolean applyConfigurationDirectives(Module module, File configurationFile, XMLConfiguration xmlconfig) throws ConfigurationException
233    {
234        boolean directiveApplied = false;
235        File configurationDirectivesDir = module.getConfigurationDirectivesDir();
236        if( !configurationDirectivesDir.isDirectory() )
237            return directiveApplied;
238        String configurationFilename = configurationFile.getName();
239        boolean useDefaultNames = configurationFilename.equals("configuration.xml");
240        String addDirectivesFilename = useDefaultNames ? "add.xml" : configurationFilename + "-add.xml";
241        String changeDirectivesFilename = useDefaultNames ? "change.xml" : configurationFilename + "-change.xml";
242        String removeDirecivesFilename = useDefaultNames ? "remove.xml" : configurationFilename + "-remove.xml";
243        File addDirectivesFile = new File(configurationDirectivesDir, addDirectivesFilename);
244        if( addDirectivesFile.exists() )
245            directiveApplied = applyAddDirectives(module, addDirectivesFile, xmlconfig);
246        File changeDirectivesFile = new File(configurationDirectivesDir, changeDirectivesFilename);
247        if( changeDirectivesFile.exists() )
248            directiveApplied = applyChangeDirectives(module, changeDirectivesFile, xmlconfig);
249        File removeDirectivesFile = new File(configurationDirectivesDir, removeDirecivesFilename);
250        if( removeDirectivesFile.exists() )
251            directiveApplied = applyRemoveDirectives(module, removeDirectivesFile, xmlconfig);
252        return directiveApplied;
253    }
254
255    public static String trimmedKey(String key)
256    {
257        if( !key.contains(".") )
258            return key;
259        return key.substring(key.indexOf('.') + 1, key.length());
260    }
261
262    public static boolean addMatch(List<String> addKeys, List<String> addedKeys, XMLConfiguration addXmlConfig, XMLConfiguration addedXmlConfig)
263    {
264        if( addKeys.size() != addedKeys.size() )
265            return false;
266
267        HashMap<String, String> matchedKeys = new HashMap<String,String>();
268        for( String addKey : addKeys )
269        {
270            boolean keyMatchFound = false;
271            for( String addedKey : addedKeys )
272            {
273                if( trimmedKey(addKey).equals(trimmedKey(addedKey)) )
274                {
275                    keyMatchFound = true;
276                    matchedKeys.put(addKey, addedKey);
277                }
278            }
279            if( !keyMatchFound )
280                return false;
281        }
282
283        for( Map.Entry<String,String> entry : matchedKeys.entrySet() )
284        {
285            Object addProperty = addXmlConfig.getProperty(entry.getKey());
286            Object addedProperty = addedXmlConfig.getProperty(entry.getValue());
287
288            if( !addProperty.equals(addedProperty) )
289                return false;
290        }
291
292        return true;
293    }
294
295
296    /**
297     * Author: David Welker
298     *
299     * This method applies unrun add directives to the configuration.
300     * @param addXml
301     * @param xmlconfig
302     */
303    private boolean applyAddDirectives(Module module, File addXml, XMLConfiguration xmlconfig) throws ConfigurationException
304    {
305        String addXmlFilename = addXml.getName();
306        String addedXmlFilename = addXmlFilename.substring(0,addXmlFilename.length()-4) + "ed.xml";
307
308        File addedXml = new File(DotKeplerManager.getInstance().getModuleConfigurationDirectory(module.getName()), addedXmlFilename);
309
310        Iterator i;
311
312        XMLConfiguration addXmlConfig = new XMLConfiguration();
313        addXmlConfig.setDelimiterParsingDisabled(true);
314        addXmlConfig.load(addXml);
315
316        XMLConfiguration addedXmlConfig = new XMLConfiguration();
317        addedXmlConfig.setDelimiterParsingDisabled(true);
318        if( addedXml.exists() )
319            addedXmlConfig.load(addedXml);
320
321        i = addXmlConfig.getKeys();
322        if( !i.hasNext() )
323            return false;
324
325        List<String> firstParts = new ArrayList<String>();
326        while( i.hasNext() )
327        {
328            String key = (String)i.next();
329            if( key.contains(".") )
330            {
331                String candidate = key.substring(0, key.indexOf('.'));
332                if( !firstParts.contains(candidate))
333                    firstParts.add(candidate);
334            }
335        }
336
337        for( String firstPart : firstParts )
338        {
339
340            int maxAddIndex = addXmlConfig.getMaxIndex(firstPart);
341            int maxAddedIndex = addedXmlConfig.getMaxIndex(firstPart);
342            int addIndex = xmlconfig.getMaxIndex(firstPart) + 1;
343
344            List<String> removeKeys = new ArrayList<String>();
345            for( int j = 0; j <= maxAddIndex; j++ )
346            {
347                List<String> addKeys = new ArrayList<String>();
348                Iterator x1 = addXmlConfig.getKeys(firstPart+"("+j+")");
349                while( x1.hasNext() )
350                {
351                    String key = (String)x1.next();
352                    addKeys.add(key);
353                }
354                for( int k = 0; k <= maxAddedIndex; k++ )
355                {
356                    List<String> addedKeys = new ArrayList<String>();
357                    Iterator x2 = addedXmlConfig.getKeys(firstPart+"("+k+")");
358                    while( x2.hasNext() )
359                    {
360                        String key = (String)x2.next();
361                        addedKeys.add(key);
362                    }
363
364                    if( addMatch(addKeys, addedKeys, addXmlConfig, addedXmlConfig) )
365                    {
366                        for( String addKey : addKeys )
367                            removeKeys.add(addKey);
368                    }
369                }
370            }
371            for( int j = removeKeys.size() - 1; j >= 0; j-- )
372            {
373                String removeKey = removeKeys.get(j);
374                addXmlConfig.clearProperty(removeKey);
375            }
376
377            for( int j = 0; j <= maxAddIndex; j++ )
378            {
379                String addXMLKey = firstPart + "("+j+")";
380                i = addXmlConfig.getKeys(addXMLKey);
381                while( i.hasNext() )
382                {
383                    String addXmlConfigKey = (String)i.next();
384                    String lastPart = addXmlConfigKey.substring(addXmlConfigKey.indexOf('.')+1,addXmlConfigKey.length());
385                    String originalXmlConfigKey = firstPart + "("+(addIndex+j)+")."+lastPart;
386                    String addedXmlConfigKey = firstPart + "("+(maxAddedIndex+1+j)+")."+lastPart;
387                    xmlconfig.addProperty(originalXmlConfigKey, addXmlConfig.getProperty(addXmlConfigKey));
388                    addedXmlConfig.addProperty(addedXmlConfigKey, addXmlConfig.getProperty(addXmlConfigKey));
389                }
390            }
391        }
392
393        List<String> addedKeys = new ArrayList<String>();
394        i = addedXmlConfig.getKeys();
395        while( i.hasNext() )
396            addedKeys.add((String)i.next());
397
398        i = addXmlConfig.getKeys();
399        while( i.hasNext() )
400        {
401            String addKey = (String)i.next();
402            if( addKey.contains(".") )
403                continue;
404            Object value = addXmlConfig.getProperty(addKey);
405            if( addedKeys.contains(addKey) )
406            {
407                if( addedXmlConfig.getProperty(addKey).equals(value) )
408                    continue;
409            }
410
411            xmlconfig.addProperty(addKey, value);
412            addedXmlConfig.addProperty(addKey, value);
413        }
414
415        addedXmlConfig.save(addedXml);
416        return true;
417
418    }
419
420    //David Welker - Not Implemented - Further Discussion Needed
421    private boolean applyChangeDirectives(Module module, File changeXml, XMLConfiguration xmlconfig) throws ConfigurationException
422    {
423        return false;
424    }
425
426    //David Welker - Not Implemented - Further Discussion Needed
427    private boolean applyRemoveDirectives(Module module, File removeXml, XMLConfiguration xmlconfig) throws ConfigurationException
428    {
429        return false;
430    }
431
432    /**
433   * return true if this file should be loaded.  if it has a locale designation
434   * that matches the given locale, return true.  If it does not contain a 
435   * locale designation and the locale is en_US return true.  If the locale is 
436   * not en_US, but there are no other config files with the proper designator,
437   * then return true.
438   */
439  private static boolean loadThisFile(File f, Locale l)
440  {
441    //System.out.println("looking for file " + f.getName() + " with locale " + l.toString());
442    
443    // see if the locale matches the locale in the file,
444    // or the locale is en_US and file has no locale
445    if(checkLocaleDesignator(f, l))
446    {
447      return true;
448    }
449    else
450    { //see if there is a file to load that isn't this one
451      File dir = new File(f.getParent());
452      String s[] = dir.list();
453      boolean filefound = false;
454          String baseName = getBaseName(f);
455      for(int i=0; i<s.length; i++)
456      {
457          //if(f.getName().startsWith("uiMenuMap")) System.out.println("    checking possibility " + s[i]); 
458
459          File possibleFile = new File(dir, s[i]);
460          // basename must match
461          String possibleBaseName = getBaseName(possibleFile);
462          if (!possibleBaseName.equals(baseName)) {
463                  //if(f.getName().startsWith("uiMenuMap")) System.out.println("    base name does not match for " + s[i]);
464                  continue;
465          }
466          // see if the other possibility has a matching locale,
467          // or no locale and our locale is en_US. in this case,
468          // this file should be loaded instead
469          if(checkLocaleDesignator(possibleFile, l)) {
470                  filefound = true;
471                  //System.out.println("  " + s[i] + " should be loaded instead");
472                  break;
473          }
474                  //else if(f.getName().startsWith("uiMenuMap")) System.out.println("  checkLocaleDesignator fails");
475
476      }
477      
478      if(filefound)
479      {
480        //there is another config file that should get loaded for this locale
481        //so don't load this one.
482        return false;
483      }
484    }
485
486    /*
487        if(f.getName().startsWith("uiMenuMap")) {
488                System.out.println("  no exact match.");
489                System.out.println("    gld = " + getLocaleDesignator(f));
490                System.out.println("    nocm = " + noOtherConfigurationMatch(f, l));
491        }
492        */
493
494    //there is no exact match for what we want
495    if(getLocaleDesignator(f) == null || noOtherConfigurationMatch(f, l))
496    { //if this is a default file, use it
497      //System.out.println(" no exact match; using this since it's default.");
498      return true;
499    }
500    else
501    { //if not, don't
502      return false;
503    }
504  }
505  
506  /**
507   * return true if this is the only configuration file with its stem name
508   * or if this configuration file is for en_US and there are no other
509   * configuration files with the same base name with a matching locale.
510   */
511  private static boolean noOtherConfigurationMatch(File f, Locale l)
512  {
513    File dir = f.getParentFile();
514    String[] list = dir.list();
515    String fBasename = getBaseName(f);
516    
517    // NOTE: if this file is en_US and l is not en_US, return true
518    // since we should use this file. this method is only called
519    // after we have checked all other possibilities.
520    String localeStr = getLocaleDesignator(f);
521    if(localeStr != null && localeStr.equals("en_US") && !l.toString().equals("en_US"))
522    {
523        return true;
524    }
525    
526    
527    for(int i=0; i<list.length; i++)
528    {
529      File dirF = new File(dir, list[i]);
530      if(dirF.getAbsolutePath().equals(f.getAbsolutePath()))
531      { //don't look at the actual file, just its siblings
532        continue;
533      }
534      String basename = getBaseName(dirF);
535      if(basename.equals(fBasename))
536      {
537          return false;
538      }
539    }
540    
541    
542    return true;
543  }
544  
545  /**
546   * return true if the designator on the file matches the locale exactly
547   */
548  private static boolean checkLocaleDesignator(File f, Locale l)
549  {
550    String designator = getLocaleDesignator(f);
551    
552    //System.out.println("designator: " + designator);
553    //System.out.println("locale: " + l.toString());
554    //System.out.println("File: " + f.getName());
555    
556    if(designator == null && l.toString().equals("en_US"))
557    { //if there is no designator and the locale is en_US, use the file
558      return true;
559    }
560    
561    //the designator is not null, see if this file matches the designator
562    if(designator != null && designator.equals(l.toString()))
563    { //load this file!
564      return true;
565    }
566    
567    return false;
568  }
569  
570  /**
571   * return the locale designator from a filename, or null if there isn't one
572   */
573  private static String getLocaleDesignator(File f)
574  {
575    String basename = ConfigurationManager.removeFileExtension(f.getName());
576    String designator = null;
577    if(basename.length() > 7)
578    {
579      designator = basename.substring(basename.length() - 6, basename.length());
580      if(designator.charAt(0) != '_' || designator.charAt(3) != '_')
581      { //these characters do not represnet a locale 
582        designator = null;
583      }
584      else
585      {
586        designator = designator.substring(1, designator.length());
587      }
588    }
589    return designator;
590  }
591  
592  /**
593   * return the base name of a file without an extension or locale designator
594   * @param f
595   */
596  private static String getBaseName(File f)
597  {
598    String basename = ConfigurationManager.removeFileExtension(f.getName());
599    String designator = null;
600    if(basename.length() > 7)
601    {
602      designator = basename.substring(basename.length() - 6, basename.length());
603      if(designator.charAt(0) != '_' || designator.charAt(3) != '_')
604      { //these characters do not represnet a locale 
605        return basename;
606      }
607      else
608      {
609        return basename.substring(0, basename.length() - 6);
610      }
611    }
612    return basename;
613  }
614  
615  /**
616   * add the structure of root to prop
617   */
618  private ConfigurationProperty getConfiguration(ConfigurationNode root, Module m, ConfigurationNamespace namespace)
619    throws Exception
620  {
621    String originMod;
622    boolean originOk = true;
623    ConfigurationProperty cp = new ConfigurationProperty(m, root.getName());
624    cp.setNamespace(namespace);
625    String value = (String)root.getValue();
626    boolean mutable = true;
627    if(value != null && !value.equals(""))
628    {
629      cp.setValue(value);
630    }
631    
632    if(root.getChildrenCount() != 0)
633    {
634      Iterator it = root.getChildren().iterator();
635      while(it.hasNext())
636      {
637        ConfigurationNode child = (ConfigurationNode)it.next();
638        ConfigurationProperty nextProp = getConfiguration(child, m, namespace);
639        if(nextProp == null)
640        {
641            if(_isDebugging)
642            {
643              _log.debug("not loading property " + child.getName() +
644                " because it was added from an inactive module.");
645            }
646          continue;
647        }
648        
649        if(nextProp.getName().equals("mutable"))
650        {
651          if(nextProp.getValue() != null && 
652             nextProp.getValue().equals("false"))
653          {
654            cp.setMutable(false);
655          }
656        }
657        else if(nextProp.getName().equals("originModule"))
658        {
659          if(nextProp.getValue() != null &&
660             !nextProp.getValue().equals(""))
661          {
662            originMod = nextProp.getValue();
663            if(!ModuleTree.instance().contains(originMod))
664            {
665              originOk = false;
666            }
667            else
668            {
669              cp.setOriginModule(ConfigurationManager.getModule(originMod));
670            }
671          }
672        }
673        
674        cp.addProperty(nextProp, true);
675      }
676    }
677    
678    if(originOk)
679    {
680      return cp;
681    }
682    else
683    {
684      return null;
685    }
686  }
687}