/*
* 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;
}
}