/**
* Copyright (C) 2010 BonitaSoft S.A.
* BonitaSoft, 31 rue Gustave Eiffel - 38000 Grenoble
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2.0 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.bonitasoft.forms.server.accessor.impl.util;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bonitasoft.console.common.server.utils.BPMEngineException;
import org.bonitasoft.console.common.server.utils.FormsResourcesUtils;
import org.bonitasoft.engine.bpm.process.ProcessDefinitionNotFoundException;
import org.bonitasoft.engine.exception.RetrieveException;
import org.bonitasoft.engine.session.APISession;
import org.bonitasoft.engine.session.InvalidSessionException;
import org.bonitasoft.forms.server.accessor.DefaultFormsPropertiesFactory;
import org.bonitasoft.forms.server.exception.InvalidFormDefinitionException;
import org.w3c.dom.Document;
/**
* The Form definition file document builder
*
* @author Anthony Birembaut, Nicolas Chabanoles
*
*/
public class FormDocumentBuilder {
/**
* The form definition file name
*/
public final static String FORM_DEFINITION_DEFAULT_FILE_NAME = "forms.xml";
/**
* The form definition file name prefixe
*/
public final static String FORM_DEFINITION_FILE_PREFIX = "forms_";
/**
* The form definition file name suffixe
*/
public final static String FORM_DEFINITION_FILE_SUFFIX = ".xml";
/**
* The document for the process definition UUID
*/
protected Document document;
/**
* The process definition UUID
*/
protected long processDefinitionID;
/**
* The locale as a string
*/
protected String locale;
/**
* Last access to the current instance
*/
protected Long lastAccess = new Date().getTime();
/**
* the {@link Date} of the process deployment
*/
protected Date processDeployementDate;
/**
* indicate if the form definition file should be retrieved from the business archive only
*/
protected final boolean getFormDefinitionFromBAR;
/**
* Logger
*/
private static Logger LOGGER = Logger.getLogger(FormDocumentBuilder.class.getName());
/**
* Instances map by process
*/
private static Map<String, Map<String, FormDocumentBuilder>> INSTANCES;
/**
* Separator for the instance map keys
*/
protected final static String INSTANCES_MAP_SEPERATOR = "@";
/**
* Retrieve an instance of FormDocumentBuilder or create a new one if necessary.
* The map contains a cache of instances. Each instance has a validity duration equals to the INSTANCE_EXPIRATION_TIME constant value
* The deployment date is also check because a process can be undeployed and redeployed (after modifications) with the same UUID
*
* @param session
* the engine API session
* @param processDefinitionID
* the process definition ID
* @param locale
* the user's locale
* @param processDeployementDate
* the deployment date of the process
* @throws IOException
* @throws InvalidFormDefinitionException
* @throws BPMEngineException
* @throws InvalidSessionException
*/
public static synchronized FormDocumentBuilder getInstance(final APISession session, final long processDefinitionID, final String locale,
final Date processDeployementDate) throws ProcessDefinitionNotFoundException, IOException, InvalidFormDefinitionException, BPMEngineException,
InvalidSessionException, RetrieveException {
return getInstance(session, processDefinitionID, locale, processDeployementDate, false);
}
/**
* Retrieve an instance of FormDocumentBuilder or create a new one if necessary.
* The map contains a cache of instances. Each instance has a validity duration equals to the INSTANCE_EXPIRATION_TIME constant value
* The deployment date is also check because a process can be undeployed and redeployed (after modifications) with the same UUID
*
* @param session
* the engine API session
* @param processDefinitionID
* the process definition ID
* @param locale
* the user's locale
* @param processDeployementDate
* the deployment date of the process
* @param getFormDefinitionFromBAR
* indicate if the form definition file should be retrieved from the business archive only (if false, it's sought in the classpath first)
* @throws IOException
* @throws InvalidFormDefinitionException
* @throws BPMEngineException
* @throws InvalidSessionException
*/
public static synchronized FormDocumentBuilder getInstance(final APISession session, final long processDefinitionID, final String locale,
final Date processDeployementDate, final boolean getFormDefinitionFromBAR) throws ProcessDefinitionNotFoundException, IOException,
InvalidFormDefinitionException, BPMEngineException, InvalidSessionException, RetrieveException {
final long tenantID = session.getTenantId();
if (INSTANCES == null) {
INSTANCES = new LinkedHashMap<String, Map<String, FormDocumentBuilder>>(DefaultFormsPropertiesFactory.getDefaultFormProperties(tenantID)
.getMaxProcessesInCache(), .75F, true) {
private static final long serialVersionUID = 7451370208143315146L;
@Override
protected boolean removeEldestEntry(final Map.Entry<String, Map<String, FormDocumentBuilder>> eldest) {
return size() > DefaultFormsPropertiesFactory.getDefaultFormProperties(tenantID).getMaxProcessesInCache();
};
};
}
FormDocumentBuilder instance = null;
if (processDefinitionID == -1) {
try {
instance = new FormDocumentBuilder(session, -1, locale, processDeployementDate, getFormDefinitionFromBAR);
} catch (final FileNotFoundException e) {
if (locale != null) {
instance = new FormDocumentBuilder(session, -1, null, processDeployementDate, getFormDefinitionFromBAR);
} else {
throw new FileNotFoundException("The forms definition file for process was not found.");
}
}
} else {
Map<String, FormDocumentBuilder> localeInstances = INSTANCES.get(processDefinitionID + INSTANCES_MAP_SEPERATOR + tenantID);
if (localeInstances != null) {
instance = localeInstances.get(locale);
}
boolean outOfDateDefinition = false;
if (instance != null
&& ((processDeployementDate != null && processDeployementDate.compareTo(instance.processDeployementDate) != 0) || instance
.hasExpired(tenantID))) {
localeInstances.remove(locale);
outOfDateDefinition = true;
}
if (instance == null || outOfDateDefinition) {
if (localeInstances == null) {
localeInstances = new LinkedHashMap<String, FormDocumentBuilder>() {
private static final long serialVersionUID = -2092174987934309788L;
@Override
protected boolean removeEldestEntry(final java.util.Map.Entry<String, FormDocumentBuilder> eldest) {
return size() > DefaultFormsPropertiesFactory.getDefaultFormProperties(tenantID).getMaxLanguagesInCache();
}
};
}
try {
instance = new FormDocumentBuilder(session, processDefinitionID, locale, processDeployementDate, getFormDefinitionFromBAR);
localeInstances.put(locale, instance);
INSTANCES.put(processDefinitionID + INSTANCES_MAP_SEPERATOR + tenantID, localeInstances);
} catch (final FileNotFoundException e) {
if (locale != null) {
instance = new FormDocumentBuilder(session, processDefinitionID, null, processDeployementDate, getFormDefinitionFromBAR);
} else {
throw new FileNotFoundException("The forms definition file for process " + processDefinitionID + "in tenant " + tenantID
+ " was not found.");
}
}
} else {
instance.lastAccess = new Date().getTime();
}
}
return instance;
}
/**
* Private constructor to prevent instantiation
*
* @param session
* the engine APISession
* @param processDefinitionID
* the process definition ID
* @param locale
* the user's locale
* @param processDeployementDate
* the deployment date of the process
* @param getFormDefinitionFromBAR
* indicate if the form definition file should be retrieved from the business archive only
* @throws IOException
* if the forms definition file is not found
* @throws InvalidFormDefinitionException
* if the form definition file cannot be parsed
* @throws BPMEngineException
* @throws InvalidSessionException
*/
protected FormDocumentBuilder(final APISession session, final long processDefinitionID, final String locale, final Date processDeployementDate,
final boolean getFormDefinitionFromBAR) throws ProcessDefinitionNotFoundException, IOException, InvalidFormDefinitionException, BPMEngineException,
InvalidSessionException, RetrieveException {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Building instance of the Form document builder for process " + processDefinitionID + " with locale " + locale
+ " and deployment date " + processDeployementDate.getTime());
}
this.processDefinitionID = processDefinitionID;
this.locale = locale;
this.processDeployementDate = processDeployementDate;
this.getFormDefinitionFromBAR = getFormDefinitionFromBAR;
final InputStream formsDefinitionStream = getFormsDefinitionInputStream(session);
try {
final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
this.document = builder.parse(formsDefinitionStream);
} catch (final Exception e) {
final String errorMessage = "Failed to parse the forms definition file";
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, errorMessage, e);
}
throw new InvalidFormDefinitionException(errorMessage);
} finally {
if (formsDefinitionStream != null) {
formsDefinitionStream.close();
}
}
}
/**
* @return the form definition as an input stream
* @param session
* the engine APISession
* @throws IOException
* @throws BPMEngineException
* @throws InvalidSessionException
*/
protected InputStream getFormsDefinitionInputStream(final APISession session) throws IOException, ProcessDefinitionNotFoundException, BPMEngineException,
InvalidSessionException, RetrieveException {
InputStream formsDefinitionInputStream = null;
URL formsParametersURL = null;
String localizedFileName = null;
if (locale != null) {
localizedFileName = FORM_DEFINITION_FILE_PREFIX + locale + FORM_DEFINITION_FILE_SUFFIX;
if (!getFormDefinitionFromBAR) {
formsParametersURL = Thread.currentThread().getContextClassLoader().getResource(localizedFileName);
}
}
if (!getFormDefinitionFromBAR && formsParametersURL == null) {
formsParametersURL = Thread.currentThread().getContextClassLoader().getResource(FORM_DEFINITION_DEFAULT_FILE_NAME);
}
if (formsParametersURL == null) {
if (processDefinitionID == -1) {
throw new FileNotFoundException("The forms definition file for the process was not found.");
}
// try to get the form file from the application resource directory where files from the bar are exctracted
final File processApplicationsResourcesDir = FormsResourcesUtils.getApplicationResourceDir(session, processDefinitionID,
processDeployementDate);
if (!processApplicationsResourcesDir.exists()) {
FormsResourcesUtils.retrieveApplicationFiles(session, processDefinitionID, processDeployementDate);
}
File formsFile = null;
if (locale != null) {
formsFile = new File(processApplicationsResourcesDir, localizedFileName);
}
if (formsFile == null || !formsFile.exists()) {
formsFile = new File(processApplicationsResourcesDir, FORM_DEFINITION_DEFAULT_FILE_NAME);
}
if (formsFile.exists()) {
formsDefinitionInputStream = new FileInputStream(formsFile);
} else {
throw new FileNotFoundException("The forms definition file for process " + processDefinitionID + " was not found.");
}
} else {
formsDefinitionInputStream = formsParametersURL.openStream();
}
return formsDefinitionInputStream;
}
/**
* @return the document
*/
public Document getDocument() {
return document;
}
/**
* Determinates whether the current instance has expired or not
*
* @param tenantID
* the tenant ID
* @return true if the current instance has expired, false otherwise
*/
protected boolean hasExpired(final long tenantID) {
final long now = new Date().getTime();
return this.lastAccess + DefaultFormsPropertiesFactory.getDefaultFormProperties(tenantID).getProcessesTimeToLiveInCache() < now;
}
}