/* * Copyright (C) 2005-2012 BetaCONCEPT Limited * * This file is part of Astroboa. * * Astroboa is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Astroboa is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Astroboa. If not, see <http://www.gnu.org/licenses/>. */ package org.betaconceptframework.astroboa.engine.definition; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.configuration.FileConfiguration; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.configuration.XMLConfiguration; import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.betaconceptframework.astroboa.api.model.CmsRepository; import org.betaconceptframework.astroboa.api.model.exception.CmsException; import org.betaconceptframework.astroboa.context.AstroboaClientContextHolder; import org.betaconceptframework.astroboa.context.RepositoryContext; import org.betaconceptframework.astroboa.engine.definition.visitor.CmsDefinitionVisitor; import org.betaconceptframework.astroboa.engine.definition.xsom.CmsEntityResolverForValidation; import org.betaconceptframework.astroboa.engine.definition.xsom.CmsXsomParserFactory; import org.betaconceptframework.astroboa.util.CmsConstants; import org.betaconceptframework.astroboa.util.CmsConstants.CmsMode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.sun.xml.xsom.XSSchema; import com.sun.xml.xsom.parser.XSOMParser; /** * Class responsible to load Content Definition Configuration settings. * Content Definition configuration contains xml file for content definition information * and a properties file containing flags for action on content definition information. * For now content definition information is reloaded only if appropriate flag is set to true and * at the same time content definition file has been Modified * * @author Gregory Chomatas (gchomatas@betaconcept.com) * @author Savvas Triantafyllou (striantafyllou@betaconcept.com) * */ public class ContentDefinitionConfiguration { private PropertiesConfiguration configuration; //Key is repository id private Map<String, List<FileConfiguration>> definitionFileConfigurations = new HashMap<String, List<FileConfiguration>>(); private CmsXsomParserFactory cmsXsomParserFactory; private CmsDefinitionVisitor definitionVisitor; private final Logger logger = LoggerFactory.getLogger(ContentDefinitionConfiguration.class); //Default value is production private CmsMode cmsMode; private List<String> builtinDefinitionSchemas; public void setBuiltinDefinitionSchemas( List<String> builtinDefinitionSchemas) { this.builtinDefinitionSchemas = builtinDefinitionSchemas; } public void setCmsXsomParserFactory(CmsXsomParserFactory cmsXsomParserFactory) { this.cmsXsomParserFactory = cmsXsomParserFactory; } public void setDefinitionVisitor(CmsDefinitionVisitor definitionVisitor) { this.definitionVisitor = definitionVisitor; } private void loadConfigurationFiles(CmsRepository associatedRepository, boolean logWarningIfNoXSDFileFound) { //Load BetaConcept Definition files from repository home directory //which exists in RepositoryContextImpl try{ File[] schemaFiles = retrieveXmlSchemaFiles(associatedRepository, logWarningIfNoXSDFileFound); //Create a file configuration for each xsd file //This is done in order to track any changes made at runtime to XSD //in order to reload definition definitionFileConfigurations.put(associatedRepository.getId(), new ArrayList<FileConfiguration>()); if (ArrayUtils.isEmpty(schemaFiles) && logWarningIfNoXSDFileFound){ logger.warn("Found no definition schema files for repository "+ associatedRepository); } else{ for (File defFile: schemaFiles){ try{ logger.debug("Loading definition file {} for repository {}", defFile.getAbsolutePath(), associatedRepository.getId()); XMLConfiguration fileConfiguration = new XMLConfiguration(defFile); fileConfiguration.setReloadingStrategy(new FileChangedReloadingStrategy()); definitionFileConfigurations.get(associatedRepository.getId()).add(fileConfiguration); } catch(Exception e){ logger.error("Error loading definition file "+defFile.getAbsolutePath()+" for repository "+ associatedRepository.getId()+"Most probably, it is not a valid XML file. All other definitions will be loaded", e); //Load an empty xml configuration XMLConfiguration fileConfiguration = new XMLConfiguration(); definitionFileConfigurations.get(associatedRepository.getId()).add(fileConfiguration); } } } } catch (Throwable e) { throw new CmsException(e); } } private File[] retrieveXmlSchemaFiles(CmsRepository associatedRepository, boolean logWarningIfNoXSDFileFound) { if (associatedRepository == null || StringUtils.isBlank(associatedRepository.getRepositoryHomeDirectory())){ throw new CmsException("Unable to locate repository home directory."+ (associatedRepository == null? "No associated repository to current thread": "Undefined repository home dir for repository "+ associatedRepository.getId())); } //Directory where definition xsd files are kept is defined from //repository home dir + directory name provided in content-definition.properties file String contentDefinitionSchemaPath = getDefinitionHomeDirPath(associatedRepository); File contentDefinitionSchemaDir = new File(contentDefinitionSchemaPath); //Check if directory exists if (!contentDefinitionSchemaDir.exists()){ if (logWarningIfNoXSDFileFound){ logger.warn("Unable to locate schema home directory {}. Only built in schemas will be loaded", contentDefinitionSchemaPath); } return new File[]{}; } //Load all xsd files return contentDefinitionSchemaDir.listFiles(new XsdFilter()); } private String getDefinitionHomeDirPath(CmsRepository associatedRepository) { String contentDefinitionSchemaPath = associatedRepository.getRepositoryHomeDirectory()+File.separator+ configuration.getString(CmsConstants.BETACONCEPT_CONTENT_DEFINITION_SCHEMA_DIR, ""); return contentDefinitionSchemaPath; } public void loadDefinitionToCache() throws Exception { RepositoryContext repositoryContext = AstroboaClientContextHolder.getRepositoryContextForActiveClient(); if (repositoryContext == null || repositoryContext.getCmsRepository() == null){ //No repository context found. Do nothing logger.warn("Unable to load definition files. No repository context found"); return; } if (mustReloadDefinitionFiles(repositoryContext.getCmsRepository())){ logger.debug("At least one definition file has been changed. Reloading takes place"); refreshContentDefinition(repositoryContext.getCmsRepository()); } } private boolean isCmsInProductionMode(){ return CmsMode.production == cmsMode; } private void refreshContentDefinition(CmsRepository associatedRepository) throws Exception { logger.debug("Reloading definition files"); //Clear cache definitionVisitor.clear(); try{ if (definitionFileConfigurations == null){ definitionFileConfigurations = new HashMap<String, List<FileConfiguration>>(); } //This ensures that warning for empty XSD directory is issued only once boolean logWarningIfNoXSDFileFound = definitionFileConfigurations.get(associatedRepository.getId())== null; if (definitionFileConfigurations.containsKey(associatedRepository.getId())){ //remove existing file configurations in order to reload REPOSITORY schemas //thus loading any new file added definitionFileConfigurations.remove(associatedRepository.getId()); } loadConfigurationFiles(associatedRepository, logWarningIfNoXSDFileFound); List<FileConfiguration> repositoryDefinitionFileConfigurations = definitionFileConfigurations.get(associatedRepository.getId()); XSOMParser xsomParser = createXsomParser(); //Feed parser with user defined schemas feedParserWithUserDefinedSchemas(xsomParser, repositoryDefinitionFileConfigurations); //Feed parser with builtin schemas feedParserWithBuiltInSchemas(xsomParser); //Load schemas to Definitions generateDefinitions(xsomParser); } catch(Exception e){ if (definitionVisitor != null){ definitionVisitor.clear(); } //Something went wrong. At least load built in schemas. try{ XSOMParser xsomParser = createXsomParser(); //Feed parser with builtin schemas feedParserWithBuiltInSchemas(xsomParser); generateDefinitions(xsomParser); } catch(Exception e1){ logger.error("Repository "+AstroboaClientContextHolder.getActiveRepositoryId()+" - Loading schemas to Astroboa Definitions failed.",e); logger.error("Repository "+AstroboaClientContextHolder.getActiveRepositoryId()+" - Loading built in only schemas as a fallback safe mechanism also failed." ,e1); if (definitionVisitor != null){ definitionVisitor.clear(); } throw e; } logger.warn("Repository "+AstroboaClientContextHolder.getActiveRepositoryId()+" - Loading schemas to Astroboa Definitions failed. Check error stack trace for more details. Neverthelss built in only schemas have been successfully loaded",e); } } private void generateDefinitions(XSOMParser xsomParser) throws SAXException, Exception { Map<String, XSSchema> schemas = new HashMap<String, XSSchema>(); int index =1; final Collection<XSSchema> schemaSet = xsomParser.getResult().getSchemas(); for (XSSchema schema: schemaSet) { final String targetNamespace = schema.getTargetNamespace(); if (StringUtils.isBlank(targetNamespace)){ //Put an index as a key schemas.put(String.valueOf(index++), schema); } else if (!schemas.containsKey(targetNamespace)){ schemas.put(schema.getTargetNamespace(), schema); } schema.visit(definitionVisitor); } //Definition Visitor will put in cache all definitions //Definition cache will obtain repository context to //correctly place definitions according to repository definitionVisitor.createContentDefintions(); } private void feedParserWithBuiltInSchemas(XSOMParser xsomParser) { if (CollectionUtils.isNotEmpty(builtinDefinitionSchemas)){ for (String builtinDefinitionSchema: builtinDefinitionSchemas){ try{ URL resource = this.getClass().getResource(builtinDefinitionSchema); //Do not parse astroboa-api.x.xsd. if (! builtinDefinitionSchema.contains(CmsConstants.ASTROBOA_API_SCHEMA_FILENAME)){ xsomParser.parse(resource); } definitionVisitor.addXMLSchemaDefinitionForFileName(resource); } catch(Exception e){ throw new CmsException("Parse error for definition file "+builtinDefinitionSchema, e); } } } } private void feedParserWithUserDefinedSchemas(XSOMParser xsomParser, List<FileConfiguration> repositoryDefinitionFileConfigurations) { List<String> absolutePathsOfFilesToExclude = new ArrayList<String>(); boolean feedParser = true; while (feedParser){ feedParser = false; //Create XSOM Parser if (xsomParser == null){ xsomParser = createXsomParser(); } for (FileConfiguration fileConf: repositoryDefinitionFileConfigurations){ if (fileConf.getFile() == null){ logger.warn("Found empty file configuration. This means that one of the XSD provided is not a valid xml. Parsing will continue for the rest of the xsds"); } else { String absolutePath = fileConf.getFile().getAbsolutePath(); if (! absolutePathsOfFilesToExclude.contains(absolutePath)){ logger.debug("Reloadding and parsing file {}", absolutePath); try{ fileConf.reload(); xsomParser.parse(fileConf.getFile()); definitionVisitor.addXMLSchemaDefinitionForFileName(FileUtils.readFileToByteArray(fileConf.getFile()), StringUtils.substringAfterLast(absolutePath, File.separator)); } catch(Exception e){ //Just issue a warning logger.warn("Parse error for definition file "+absolutePath+ " This file is excluded from building Astroboa Definitions", e); //we need to feed parser again since it sets an error flag to true //and does not produce any schemas at all. feedParser = true; absolutePathsOfFilesToExclude.add(absolutePath); xsomParser = null; definitionVisitor.clear(); break; } } } } } } private XSOMParser createXsomParser() { XSOMParser xsomParser; xsomParser = cmsXsomParserFactory.createXsomParser(); return xsomParser; } public void setConfigurationFile(String configurationFile) { if (configurationFile != null) try { configuration = new PropertiesConfiguration(configurationFile); configuration.setReloadingStrategy(new FileChangedReloadingStrategy()); final String cmsModeFromFile = configuration.getString(CmsConstants.CMS_MODE, CmsMode.production.toString()); //Set CmsMode if (cmsModeFromFile != null) cmsMode = CmsMode.valueOf(cmsModeFromFile.toLowerCase()); else cmsMode = CmsMode.production; } catch (Exception e) { logger.warn("Unable to load content definition properties file", e); } } private boolean mustReloadDefinitionFiles(CmsRepository associatedRepository) { //Check reload flag with default value set to false if (isCmsInProductionMode()){ if (definitionFileConfigurations != null && CollectionUtils.isNotEmpty(definitionFileConfigurations.get(associatedRepository.getId()))){ return false; } else{ return true; } } //Debug mode else if (configuration != null) { final boolean reloadDefinitionFiles = configuration.getBoolean(CmsConstants.RELOAD_CONTENT_DEFINITION_FILE, false); //Reloading has been disabled but no definition files have ever been loaded //It may be the first time if (! reloadDefinitionFiles && ( definitionFileConfigurations == null || ! definitionFileConfigurations.containsKey(associatedRepository.getId()) )){ return true; } if ( reloadDefinitionFiles && definitionFileConfigurations != null) { List<FileConfiguration> repositoryFileConfigurations = definitionFileConfigurations.get(associatedRepository.getId()); if (repositoryFileConfigurations == null){ return true; } //Check if any file has been added or removed //Get files that exist in definition directory File[] definitionSchemaFiles = retrieveXmlSchemaFiles(associatedRepository, false); //At least one definition was added or removed if (repositoryFileConfigurations.size() != definitionSchemaFiles.length){ return true; } //Reload flag is set to true. Check that file is changed boolean atLeastOneFileChanged = false; for(FileConfiguration fileConf: repositoryFileConfigurations){ if (fileConf.getReloadingStrategy().reloadingRequired()){ fileConf.getReloadingStrategy().reloadingPerformed(); logger.info("Definition file {} has been modified and will be reloaded for repository {}", fileConf.getFileName(), associatedRepository.getId()); atLeastOneFileChanged = true; break; } } return atLeastOneFileChanged; } } return false; } public boolean definitionFileForActiveRepositoryIsValid(String definitionToBeValidated, String definitionFileName) throws Exception{ if (StringUtils.isBlank(definitionToBeValidated) || StringUtils.isBlank(definitionFileName)){ return false; } RepositoryContext repositoryContext = AstroboaClientContextHolder.getRepositoryContextForActiveClient(); if (repositoryContext == null || repositoryContext.getCmsRepository() == null){ //No repository context found. Do nothing logger.warn("Unable to validate definition files. No repository context found"); return false; } final CmsRepository associatedRepository = repositoryContext.getCmsRepository(); File[] existingDefinitionFiles = retrieveXmlSchemaFiles(associatedRepository, false); CmsEntityResolverForValidation entityResolverForValidation = null; XSOMParser xsomParser = null; List<InputStream> openStreams = new ArrayList<InputStream>(); try{ entityResolverForValidation = createEntityResolverForValidation(existingDefinitionFiles, definitionToBeValidated, definitionFileName); //Feed parser with user defined schemas xsomParser = cmsXsomParserFactory.createXsomParserForValidation(entityResolverForValidation); //Feed parser with builtin schemas if (MapUtils.isNotEmpty(entityResolverForValidation.getDefinitionSources())){ for (Entry<String,String> definitionSource: entityResolverForValidation.getDefinitionSources().entrySet()){ if (! StringUtils.equals(CmsConstants.ASTROBOA_MODEL_SCHEMA_FILENAME_WITH_VERSION, definitionSource.getKey())){ try{ InputStream openStream = IOUtils.toInputStream(definitionSource.getValue(), "UTF-8"); openStreams.add(openStream); InputSource is = new InputSource(openStream); is.setSystemId(definitionSource.getKey()); xsomParser.parse(is); } catch(Exception e){ throw new CmsException("Parse error for definition "+definitionSource.getKey(), e); } } } } //Force parser to create XSD schemas. This way more errors can be detected xsomParser.getResult().getSchemas(); } catch(Exception e){ throw e; } finally{ xsomParser = null; if (entityResolverForValidation != null){ entityResolverForValidation.clearDefinitions(); } if (! openStreams.isEmpty()){ for (InputStream is : openStreams){ IOUtils.closeQuietly(is); } } } return true; } private CmsEntityResolverForValidation createEntityResolverForValidation(File[] existingDefinitionFiles, String definitionToBeValidated, String definitionFileName) throws IOException { CmsEntityResolverForValidation entityResolver = new CmsEntityResolverForValidation(); boolean definitionToBeValidatedHasBeenAdded = false; for (File existingDefinitionFile : existingDefinitionFiles){ if (StringUtils.equals(definitionFileName, existingDefinitionFile.getName())){ entityResolver.addDefinition(definitionFileName, definitionToBeValidated); definitionToBeValidatedHasBeenAdded = true; } else{ entityResolver.addExternalDefinition(existingDefinitionFile); } } if (! definitionToBeValidatedHasBeenAdded){ //New definition entityResolver.addDefinition(definitionFileName, definitionToBeValidated); } return entityResolver; } }