From 306c8b0cbe3d4417e6892537a76e7615e4b928e9 Mon Sep 17 00:00:00 2001 From: thomas dumont Date: Thu, 6 Sep 2018 20:38:36 +0200 Subject: [PATCH] LUTECE-2213 : Add the possibility to trigger deamons using cron patterns with job scheduler service --- .../portal/business/daemon/DaemonTrigger.java | 155 ++++++ .../business/daemon/DaemonTriggerDAO.java | 168 +++++++ .../business/daemon/DaemonTriggerHome.java | 110 +++++ .../business/daemon/IDaemonTriggerDAO.java | 82 +++ .../resources/system_messages.properties | 54 ++ .../resources/system_messages_fr.properties | 54 ++ .../service/daemon/AppDaemonService.java | 22 + .../portal/service/daemon/DaemonJob.java | 78 +++ .../lutece/portal/service/init/AppInit.java | 4 + .../scheduler/JobSchedulerService.java | 205 +++++++- .../portal/web/system/TriggersJspBean.java | 379 ++++++++++++++ src/sql/create_db_lutece_core.sql | 13 + .../update_db_lutece_core-6.1.1-6.1.2.sql | 12 + .../daemon/DaemonTriggerHomeTest.java | 91 ++++ .../scheduler/JobSchedulerServiceTest.java | 95 ++++ .../web/system/TriggersJspBeanTest.java | 467 ++++++++++++++++++ webapp/WEB-INF/conf/core_context.xml | 2 + webapp/WEB-INF/conf/daemons.properties | 6 + .../admin/system/manage_daemons.html | 6 +- .../admin/trigger/create_trigger.html | 27 + .../admin/trigger/manage_triggers.html | 53 ++ .../admin/trigger/modify_trigger.html | 27 + webapp/jsp/admin/system/ManageTriggers.jsp | 10 + 23 files changed, 2118 insertions(+), 2 deletions(-) create mode 100644 src/java/fr/paris/lutece/portal/business/daemon/DaemonTrigger.java create mode 100644 src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerDAO.java create mode 100644 src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHome.java create mode 100644 src/java/fr/paris/lutece/portal/business/daemon/IDaemonTriggerDAO.java create mode 100644 src/java/fr/paris/lutece/portal/service/daemon/DaemonJob.java create mode 100644 src/java/fr/paris/lutece/portal/web/system/TriggersJspBean.java create mode 100644 src/sql/upgrade/update_db_lutece_core-6.1.1-6.1.2.sql create mode 100644 src/test/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHomeTest.java create mode 100644 src/test/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerServiceTest.java create mode 100644 src/test/java/fr/paris/lutece/portal/web/system/TriggersJspBeanTest.java create mode 100644 webapp/WEB-INF/templates/admin/trigger/create_trigger.html create mode 100644 webapp/WEB-INF/templates/admin/trigger/manage_triggers.html create mode 100644 webapp/WEB-INF/templates/admin/trigger/modify_trigger.html create mode 100644 webapp/jsp/admin/system/ManageTriggers.jsp diff --git a/src/java/fr/paris/lutece/portal/business/daemon/DaemonTrigger.java b/src/java/fr/paris/lutece/portal/business/daemon/DaemonTrigger.java new file mode 100644 index 0000000000..19d3821454 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/business/daemon/DaemonTrigger.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.business.daemon; + +import java.io.Serializable; +import javax.validation.constraints.Size; +import org.hibernate.validator.constraints.NotEmpty; + +/** + * This is the business class for the object DaemonTrigger + */ +public class DaemonTrigger implements Serializable +{ + private static final long serialVersionUID = 1L; + + // Variables declarations + private int _nId; + + @NotEmpty( message = "#i18n{portal.system.validation.daemontrigger.Key.notEmpty}" ) + @Size( max = 50 , message = "#i18n{portal.system.validation.daemontrigger.Key.size}" ) + private String _strKey; + + @NotEmpty( message = "#i18n{portal.system.validation.daemontrigger.Group.notEmpty}" ) + @Size( max = 50 , message = "#i18n{portal.system.validation.daemontrigger.Group.size}" ) + private String _strGroup; + + @NotEmpty( message = "#i18n{portal.system.validation.daemontrigger.CronExpression.notEmpty}" ) + @Size( max = 50 , message = "#i18n{portal.system.validation.daemontrigger.CronExpression.size}" ) + private String _strCronExpression; + + @NotEmpty( message = "#i18n{portal.system.validation.daemontrigger.DaemonKey.notEmpty}" ) + @Size( max = 50 , message = "#i18n{portal.system.validation.daemontrigger.DaemonKey.size}" ) + private String _strDaemonKey; + + /** + * Returns the Id + * @return The Id + */ + public int getId( ) + { + return _nId; + } + + /** + * Sets the Id + * @param nId The Id + */ + public void setId( int nId ) + { + _nId = nId; + } + + /** + * Returns the Key + * @return The Key + */ + public String getKey( ) + { + return _strKey; + } + + /** + * Sets the Key + * @param strKey The Key + */ + public void setKey( String strKey ) + { + _strKey = strKey; + } + + /** + * Returns the Group + * @return The Group + */ + public String getGroup( ) + { + return _strGroup; + } + + /** + * Sets the Group + * @param strGroup The Group + */ + public void setGroup( String strGroup ) + { + _strGroup = strGroup; + } + + /** + * Returns the CronExpression + * @return The CronExpression + */ + public String getCronExpression( ) + { + return _strCronExpression; + } + + /** + * Sets the CronExpression + * @param strCronExpression The CronExpression + */ + public void setCronExpression( String strCronExpression ) + { + _strCronExpression = strCronExpression; + } + + /** + * Returns the DaemonKey + * @return The DaemonKey + */ + public String getDaemonKey( ) + { + return _strDaemonKey; + } + + /** + * Sets the DaemonKey + * @param strDaemonKey The DaemonKey + */ + public void setDaemonKey( String strDaemonKey ) + { + _strDaemonKey = strDaemonKey; + } +} diff --git a/src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerDAO.java b/src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerDAO.java new file mode 100644 index 0000000000..08809ce3c1 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerDAO.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.business.daemon; + +import fr.paris.lutece.portal.service.plugin.Plugin; +import fr.paris.lutece.util.sql.DAOUtil; +import java.sql.Statement; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides Data Access methods for DaemonTrigger objects + */ +public final class DaemonTriggerDAO implements IDaemonTriggerDAO +{ + // Constants + private static final String SQL_QUERY_SELECT = "SELECT id_daemon_trigger, trigger_key, trigger_group, cron_expression, daemon_key FROM core_daemon_trigger WHERE id_daemon_trigger = ?"; + private static final String SQL_QUERY_INSERT = "INSERT INTO core_daemon_trigger ( trigger_key, trigger_group, cron_expression, daemon_key ) VALUES ( ?, ?, ?, ? ) "; + private static final String SQL_QUERY_DELETE = "DELETE FROM core_daemon_trigger WHERE id_daemon_trigger = ? "; + private static final String SQL_QUERY_UPDATE = "UPDATE core_daemon_trigger SET id_daemon_trigger = ?, trigger_key = ?, trigger_group = ?, cron_expression = ?, daemon_key = ? WHERE id_daemon_trigger = ?"; + private static final String SQL_QUERY_SELECTALL = "SELECT id_daemon_trigger, trigger_key, trigger_group, cron_expression, daemon_key FROM core_daemon_trigger"; + + /** + * {@inheritDoc } + */ + @Override + public void insert( DaemonTrigger daemonTrigger, Plugin plugin ) + { + DAOUtil daoUtil = new DAOUtil( SQL_QUERY_INSERT, Statement.RETURN_GENERATED_KEYS, plugin ); + try + { + int nIndex = 1; + daoUtil.setString( nIndex++ , daemonTrigger.getKey( ) ); + daoUtil.setString( nIndex++ , daemonTrigger.getGroup( ) ); + daoUtil.setString( nIndex++ , daemonTrigger.getCronExpression( ) ); + daoUtil.setString( nIndex++ , daemonTrigger.getDaemonKey( ) ); + + daoUtil.executeUpdate( ); + if ( daoUtil.nextGeneratedKey( ) ) + { + daemonTrigger.setId( daoUtil.getGeneratedKeyInt( 1 ) ); + } + } + finally + { + daoUtil.free( ); + } + } + + /** + * {@inheritDoc } + */ + @Override + public DaemonTrigger load( int nKey, Plugin plugin ) + { + DAOUtil daoUtil = new DAOUtil( SQL_QUERY_SELECT, plugin ); + daoUtil.setInt( 1 , nKey ); + daoUtil.executeQuery( ); + DaemonTrigger daemonTrigger = null; + + if ( daoUtil.next( ) ) + { + daemonTrigger = new DaemonTrigger(); + int nIndex = 1; + + daemonTrigger.setId( daoUtil.getInt( nIndex++ ) ); + daemonTrigger.setKey( daoUtil.getString( nIndex++ ) ); + daemonTrigger.setGroup( daoUtil.getString( nIndex++ ) ); + daemonTrigger.setCronExpression( daoUtil.getString( nIndex++ ) ); + daemonTrigger.setDaemonKey( daoUtil.getString( nIndex++ ) ); + } + + daoUtil.free( ); + return daemonTrigger; + } + + /** + * {@inheritDoc } + */ + @Override + public void delete( int nKey, Plugin plugin ) + { + DAOUtil daoUtil = new DAOUtil( SQL_QUERY_DELETE, plugin ); + daoUtil.setInt( 1 , nKey ); + daoUtil.executeUpdate( ); + daoUtil.free( ); + } + + /** + * {@inheritDoc } + */ + @Override + public void store( DaemonTrigger daemonTrigger, Plugin plugin ) + { + DAOUtil daoUtil = new DAOUtil( SQL_QUERY_UPDATE, plugin ); + int nIndex = 1; + + daoUtil.setInt( nIndex++ , daemonTrigger.getId( ) ); + daoUtil.setString( nIndex++ , daemonTrigger.getKey( ) ); + daoUtil.setString( nIndex++ , daemonTrigger.getGroup( ) ); + daoUtil.setString( nIndex++ , daemonTrigger.getCronExpression( ) ); + daoUtil.setString( nIndex++ , daemonTrigger.getDaemonKey( ) ); + daoUtil.setInt( nIndex , daemonTrigger.getId( ) ); + + daoUtil.executeUpdate( ); + daoUtil.free( ); + } + + /** + * {@inheritDoc } + */ + @Override + public List selectDaemonTriggersList( Plugin plugin ) + { + List daemonTriggerList = new ArrayList( ); + DAOUtil daoUtil = new DAOUtil( SQL_QUERY_SELECTALL, plugin ); + daoUtil.executeQuery( ); + + while ( daoUtil.next( ) ) + { + DaemonTrigger daemonTrigger = new DaemonTrigger( ); + int nIndex = 1; + + daemonTrigger.setId( daoUtil.getInt( nIndex++ ) ); + daemonTrigger.setKey( daoUtil.getString( nIndex++ ) ); + daemonTrigger.setGroup( daoUtil.getString( nIndex++ ) ); + daemonTrigger.setCronExpression( daoUtil.getString( nIndex++ ) ); + daemonTrigger.setDaemonKey( daoUtil.getString( nIndex++ ) ); + + daemonTriggerList.add( daemonTrigger ); + } + + daoUtil.free( ); + return daemonTriggerList; + } +} diff --git a/src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHome.java b/src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHome.java new file mode 100644 index 0000000000..2166b69f36 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHome.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.business.daemon; + +import fr.paris.lutece.portal.service.plugin.Plugin; +import fr.paris.lutece.portal.service.plugin.PluginService; +import fr.paris.lutece.portal.service.spring.SpringContextService; + +import java.util.List; + +/** + * This class provides instances management methods (create, find, ...) for DaemonTrigger objects + */ +public final class DaemonTriggerHome +{ + // Static variable pointed at the DAO instance + private static IDaemonTriggerDAO _dao = SpringContextService.getBean( "daemonTriggerDAO" ); + private static Plugin _plugin = PluginService.getPlugin( "daemontrigger" ); + + /** + * Private constructor - this class need not be instantiated + */ + private DaemonTriggerHome( ) + { + } + + /** + * Create an instance of the daemonTrigger class + * @param daemonTrigger The instance of the DaemonTrigger which contains the informations to store + * @return The instance of daemonTrigger which has been created with its primary key. + */ + public static DaemonTrigger create( DaemonTrigger daemonTrigger ) + { + _dao.insert( daemonTrigger, _plugin ); + + return daemonTrigger; + } + + /** + * Update of the daemonTrigger which is specified in parameter + * @param daemonTrigger The instance of the DaemonTrigger which contains the data to store + * @return The instance of the daemonTrigger which has been updated + */ + public static DaemonTrigger update( DaemonTrigger daemonTrigger ) + { + _dao.store( daemonTrigger, _plugin ); + + return daemonTrigger; + } + + /** + * Remove the daemonTrigger whose identifier is specified in parameter + * @param nKey The daemonTrigger Id + */ + public static void remove( int nKey ) + { + _dao.delete( nKey, _plugin ); + } + + /** + * Returns an instance of a daemonTrigger whose identifier is specified in parameter + * @param nKey The daemonTrigger primary key + * @return an instance of DaemonTrigger + */ + public static DaemonTrigger findByPrimaryKey( int nKey ) + { + return _dao.load( nKey, _plugin ); + } + + /** + * Load the data of all the daemonTrigger objects and returns them as a list + * @return the list which contains the data of all the daemonTrigger objects + */ + public static List getDaemonTriggersList( ) + { + return _dao.selectDaemonTriggersList( _plugin ); + } +} + diff --git a/src/java/fr/paris/lutece/portal/business/daemon/IDaemonTriggerDAO.java b/src/java/fr/paris/lutece/portal/business/daemon/IDaemonTriggerDAO.java new file mode 100644 index 0000000000..8ad84542bf --- /dev/null +++ b/src/java/fr/paris/lutece/portal/business/daemon/IDaemonTriggerDAO.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.business.daemon; + +import fr.paris.lutece.portal.service.plugin.Plugin; +import java.util.List; + +/** + * IDaemonTriggerDAO Interface + */ +public interface IDaemonTriggerDAO +{ + /** + * Insert a new record in the table. + * @param daemonTrigger instance of the DaemonTrigger object to insert + * @param plugin the Plugin + */ + void insert( DaemonTrigger daemonTrigger, Plugin plugin ); + + /** + * Update the record in the table + * @param daemonTrigger the reference of the DaemonTrigger + * @param plugin the Plugin + */ + void store( DaemonTrigger daemonTrigger, Plugin plugin ); + + /** + * Delete a record from the table + * @param nKey The identifier of the DaemonTrigger to delete + * @param plugin the Plugin + */ + void delete( int nKey, Plugin plugin ); + + /////////////////////////////////////////////////////////////////////////// + // Finders + + /** + * Load the data from the table + * @param nKey The identifier of the daemonTrigger + * @param plugin the Plugin + * @return The instance of the daemonTrigger + */ + DaemonTrigger load( int nKey, Plugin plugin ); + + /** + * Load the data of all the daemonTrigger objects and returns them as a list + * @param plugin the Plugin + * @return The list which contains the data of all the daemonTrigger objects + */ + List selectDaemonTriggersList( Plugin plugin ); +} diff --git a/src/java/fr/paris/lutece/portal/resources/system_messages.properties b/src/java/fr/paris/lutece/portal/resources/system_messages.properties index 8d2fa71625..9f75e36ee5 100644 --- a/src/java/fr/paris/lutece/portal/resources/system_messages.properties +++ b/src/java/fr/paris/lutece/portal/resources/system_messages.properties @@ -11,6 +11,8 @@ adminFeature.plugins_management.name=Plugins management adminFeature.plugins_management.description=Enable/disable plugins and sets DB properties adminFeature.daemons_management.name=Daemons management adminFeature.daemons_management.description=View daemons running informations +adminFeature.triggers_management.name=Triggers management +adminFeature.triggers_management.description=Create, modify or delete triggers adminFeature.external_features_management.name=External features management adminFeature.external_features_management.description=Settings for external features @@ -20,6 +22,10 @@ message.confirmDisable=Are you sur you want to disable this plugin ?
All message.portletExist=You can't disable this plugin.
Some portlets are still in use in the site. message.noCoreCompatibility=This plugin is not compatible with this version of the site (minimum version: (0), maximum version (1))! message.installError=An error occurred during installation of this plugin ! +message.triggerExistsError=This trigger already exists ! +message.invalidCronExpressionError=Cron Expression is invalid ! +message.invalidDaemonKeyError=Daemon Key is invalid ! +message.confirmRemoveTrigger=Do you really want to delete this trigger ? ################################################################################ # Deamons @@ -163,10 +169,58 @@ manage_daemons.buttonStart=Start manage_daemons.buttonStop=Stop manage_daemons.buttonRun=Run now manage_daemons.buttonUpdateInterval=Modify Interval +manage_daemons.buttonManageTriggers=Manage Triggers manage_daemons.unit.sec=s manage_daemons.unit.mn=mn manage_daemons.unit.hour=h +# Template create_trigger +create_trigger.title=Create a trigger +create_trigger.labelKey=Trigger Key +create_trigger.labelKey.help=Trigger Key +create_trigger.labelGroup=Trigger Group +create_trigger.labelGroup.help=Trigger Group +create_trigger.labelCronExpression=Cron Expression +create_trigger.labelCronExpression.help=Cron Expression +create_trigger.labelDaemonKey=Daemon Key +create_trigger.labelDaemonKey.help=Daemon Key + +# Template manage_triggers +manage_triggers.title=Trigger +manage_triggers.buttonAdd=Add +manage_triggers.buttonManageDaemons=Manage Daemons +manage_triggers.columnKey=Trigger Key +manage_triggers.columnGroup=Trigger Group +manage_triggers.columnCronExpression=Cron Expression +manage_triggers.columnDaemonKey=Daemon Key + +# Template modify_trigger +modify_trigger.title=Modify a trigger +modify_trigger.labelKey=Trigger Key +modify_trigger.labelKey.help=Trigger Key +modify_trigger.labelGroup=Trigger Group +modify_trigger.labelGroup.help=Trigger Group +modify_trigger.labelCronExpression=Cron Expression +modify_trigger.labelCronExpression.help=Cron Expression +modify_trigger.labelDaemonKey=Daemon Key +modify_trigger.labelDaemonKey.help=Daemon Key + +# JSR 303 constraint validator messages +validation.daemontrigger.Key.notEmpty=The field Trigger Key cannot be empty. Please fill it. +validation.daemontrigger.Key.size=The field Trigger Key cannot accept more than 50 characters. +validation.daemontrigger.Group.notEmpty=The field Trigger Group cannot be empty. Please fill it. +validation.daemontrigger.Group.size=The field Trigger Group cannot accept more than 50 characters. +validation.daemontrigger.CronExpression.notEmpty=The field Cron Expression cannot be empty. Please fill it. +validation.daemontrigger.CronExpression.size=The field Cron Expression cannot accept more than 50 characters. +validation.daemontrigger.DaemonKey.notEmpty=The field Daemon Key cannot be empty. Please fill it. +validation.daemontrigger.DaemonKey.size=The field Daemon Key cannot accept more than 50 characters. + +# model attributes for validation messages +model.entity.daemontrigger.attribute.key=Key +model.entity.daemontrigger.attribute.group=Group +model.entity.daemontrigger.attribute.cronExpression=Cron expression +model.entity.daemontrigger.attribute.daemonKey=Daemon key + # Template config_properties.html log4j.sendmail.subject=Error in the webapp {0} diff --git a/src/java/fr/paris/lutece/portal/resources/system_messages_fr.properties b/src/java/fr/paris/lutece/portal/resources/system_messages_fr.properties index 9d2ac04ebc..b520436284 100644 --- a/src/java/fr/paris/lutece/portal/resources/system_messages_fr.properties +++ b/src/java/fr/paris/lutece/portal/resources/system_messages_fr.properties @@ -11,6 +11,8 @@ adminFeature.plugins_management.name=Gestion des Plugins adminFeature.plugins_management.description=Gestion des plugins du portail adminFeature.daemons_management.name=Gestion des Daemons adminFeature.daemons_management.description=Visualise les param\u00e8tres d'ex\u00e9cution des daemons +adminFeature.triggers_management.name=Gestion des d\u00e9clencheurs +adminFeature.triggers_management.description=Cr\u00e9er, modifier ou supprimer des d\u00e9clencheurs de Daemon adminFeature.external_features_management.name=Gestion des Features externes adminFeature.external_features_management.description=Configuration des AdminFeatures externes @@ -23,6 +25,10 @@ message.installError=Une erreur est apparue lors de l'installation de ce plugin message.confirmToggleCacheTitle=ATTENTION ! message.confirmToggleCache=Etes-vous s\u00fbr de vouloir modifier l''activation du cache {0} ? Ceci peut avoir des cons\u00e9quences s\u00e9rieuses sur votre syst\u00e8me. message.invalidCacheId=Erreur d'identification du cache ! +message.triggerExistsError=Ce d\u00e9clencheur existe d\u00e9j\u00e0 ! +message.invalidCronExpressionError=L'expression Cron est incorrecte ! +message.invalidDaemonKeyError=La cl\u00e9 du daemon est incorrecte ! +message.confirmRemoveTrigger=Etes-vous s\u00fbr de vouloir supprimer ce d\u00e9clencheur ? ################################################################################ # Deamons @@ -162,10 +168,58 @@ manage_daemons.buttonStart=D\u00e9marrer manage_daemons.buttonStop=Arr\u00eater manage_daemons.buttonRun=Ex\u00e9cuter maintenant manage_daemons.buttonUpdateInterval=Modifier l'intervalle de passage du daemon +manage_daemons.buttonManageTriggers=G\u00e9rer les d\u00e9clencheurs manage_daemons.unit.sec=s manage_daemons.unit.mn=mn manage_daemons.unit.hour=h +# Template create_trigger +create_trigger.title=Cr\u00e9er un d\u00e9clencheur +create_trigger.labelKey=Cl\u00e9 du d\u00e9clencheur +create_trigger.labelKey.help=Cl\u00e9 du d\u00e9clencheur +create_trigger.labelGroup=Groupe du d\u00e9clencheur +create_trigger.labelGroup.help=Groupe du d\u00e9clencheur +create_trigger.labelCronExpression=Expression Cron +create_trigger.labelCronExpression.help=Expression Cron +create_trigger.labelDaemonKey=Cl\u00e9 du Daemon +create_trigger.labelDaemonKey.help=Cl\u00e9 du Daemon + +# Template manage_triggers +manage_triggers.title=D\u00e9clencheurs de Daemon +manage_triggers.buttonAdd=Ajouter +manage_triggers.buttonManageDaemons=G\u00e9rer les Daemons +manage_triggers.columnKey=Cl\u00e9 du d\u00e9clencheur +manage_triggers.columnGroup=Groupe du d\u00e9clencheur +manage_triggers.columnCronExpression=Expression Cron +manage_triggers.columnDaemonKey=Cl\u00e9 du Daemon + +# Template modify_trigger +modify_trigger.title=Modifier un d\u00e9clencheur +modify_trigger.labelKey=Cl\u00e9 du d\u00e9clencheur +modify_trigger.labelKey.help=Cl\u00e9 du d\u00e9clencheur +modify_trigger.labelGroup=Groupe du d\u00e9clencheur +modify_trigger.labelGroup.help=Groupe du d\u00e9clencheur +modify_trigger.labelCronExpression=Expression Cron +modify_trigger.labelCronExpression.help=Expression Cron +modify_trigger.labelDaemonKey=Cl\u00e9 du Daemon +modify_trigger.labelDaemonKey.help=Cl\u00e9 du Daemon + +# JSR 303 constraint validator messages +validation.daemontrigger.Key.notEmpty=Le champ Cl\u00e9 du d\u00e9clencheur ne doit pas \u00eatre vide. Veuillez le remplir SVP. +validation.daemontrigger.Key.size=Le champ Cl\u00e9 du d\u00e9clencheur ne doit pas contenir plus de 50 caract\u00e8res. +validation.daemontrigger.Group.notEmpty=Le champ Groupe du d\u00e9clencheur ne doit pas \u00eatre vide. Veuillez le remplir SVP. +validation.daemontrigger.Group.size=Le champ Groupe du d\u00e9clencheur ne doit pas contenir plus de 50 caract\u00e8res. +validation.daemontrigger.CronExpression.notEmpty=Le champ Expression Cron ne doit pas \u00eatre vide. Veuillez le remplir SVP. +validation.daemontrigger.CronExpression.size=Le champ Expression Cron ne doit pas contenir plus de 50 caract\u00e8res. +validation.daemontrigger.DaemonKey.notEmpty=Le champ Cl\u00e9 du Daemon ne doit pas \u00eatre vide. Veuillez le remplir SVP. +validation.daemontrigger.DaemonKey.size=Le champ Cl\u00e9 du Daemon ne doit pas contenir plus de 50 caract\u00e8res. + +# model attributes for validation messages +model.entity.daemontrigger.attribute.key=Cl\u00e9 du d\u00e9clencheur +model.entity.daemontrigger.attribute.group=Groupe du d\u00e9clencheur +model.entity.daemontrigger.attribute.cronExpression=Expression cron +model.entity.daemontrigger.attribute.daemonKey=Cl\u00e9 du Daemon + # Template config_properties.html log4j.sendmail.subject=Erreur dans la webapp {0} diff --git a/src/java/fr/paris/lutece/portal/service/daemon/AppDaemonService.java b/src/java/fr/paris/lutece/portal/service/daemon/AppDaemonService.java index a83fba9f2d..64043863c6 100644 --- a/src/java/fr/paris/lutece/portal/service/daemon/AppDaemonService.java +++ b/src/java/fr/paris/lutece/portal/service/daemon/AppDaemonService.java @@ -338,6 +338,28 @@ public static Collection getDaemonEntries( ) return _mapDaemonEntries.values( ); } + /** + * Check if the daemon key already exists or not + * + * @param strDaemonKey + * the daemon key + * @return true if the daemon key exists, false otherwise + */ + public static boolean checkDaemonKey( String strDaemonKey ) + { + Collection listDaemonEntries = getDaemonEntries( ); + + for ( DaemonEntry daemonEntry : listDaemonEntries ) + { + if ( daemonEntry.getId( ).equals( strDaemonKey ) ) + { + return true; + } + } + + return false; + } + /** * Performs the shutdown of the DaemonFactory. */ diff --git a/src/java/fr/paris/lutece/portal/service/daemon/DaemonJob.java b/src/java/fr/paris/lutece/portal/service/daemon/DaemonJob.java new file mode 100644 index 0000000000..7b98719b08 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/service/daemon/DaemonJob.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.service.daemon; + +import fr.paris.lutece.portal.service.plugin.PluginService; +import fr.paris.lutece.portal.service.util.AppLogService; +import java.util.Date; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +/** + * Interfaces for Daemons Service. + */ +public class DaemonJob implements Job +{ + /** + * {@inheritDoc} + */ + @Override + public void execute(JobExecutionContext context) throws JobExecutionException + { + JobDataMap dataMap = context.getTrigger( ).getJobDataMap( ); + + String strDaemonKey = dataMap.getString("DaemonKey"); + AppLogService.info( "Daemon " + strDaemonKey + " - start processing." ); + Daemon daemon = AppDaemonService.getDaemon( strDaemonKey ); + try + { + if ( PluginService.isPluginEnable( daemon.getPluginName( ) ) ) + { + daemon.run( ); + } + else + { + AppLogService.info( "Could not process Daemon " + strDaemonKey + " : Plugin not enabled" ); + } + } + catch( Throwable t ) + { + AppLogService.error( "Could not process Daemon: " + strDaemonKey, t ); + } + + AppLogService.info( "Daemon" + strDaemonKey + " - end of process." ); + } +} diff --git a/src/java/fr/paris/lutece/portal/service/init/AppInit.java b/src/java/fr/paris/lutece/portal/service/init/AppInit.java index 9cc83e9515..4538bd43ee 100644 --- a/src/java/fr/paris/lutece/portal/service/init/AppInit.java +++ b/src/java/fr/paris/lutece/portal/service/init/AppInit.java @@ -48,6 +48,7 @@ import fr.paris.lutece.portal.service.mailinglist.AdminMailingListService; import fr.paris.lutece.portal.service.plugin.PluginService; import fr.paris.lutece.portal.service.portal.PortalService; +import fr.paris.lutece.portal.service.scheduler.JobSchedulerService; import fr.paris.lutece.portal.service.search.IndexationService; import fr.paris.lutece.portal.service.security.SecurityService; import fr.paris.lutece.portal.service.servlet.ServletService; @@ -199,6 +200,9 @@ public static void initServices( ServletContext context, String strConfPath, Str // Initializes the daemons service AppDaemonService.init( ); + // Initializes the job scheduler service + JobSchedulerService.getInstance( ); + // Initializes the admin authentication module AdminAuthenticationService.init( ); diff --git a/src/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerService.java b/src/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerService.java index 0f478103a5..84344ef46b 100644 --- a/src/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerService.java +++ b/src/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2017, Mairie de Paris + * Copyright (c) 2002-2018, Mairie de Paris * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,7 +33,11 @@ */ package fr.paris.lutece.portal.service.scheduler; +import fr.paris.lutece.portal.business.daemon.DaemonTrigger; +import fr.paris.lutece.portal.business.daemon.DaemonTriggerHome; +import fr.paris.lutece.portal.service.daemon.DaemonJob; import fr.paris.lutece.portal.service.util.AppLogService; +import java.util.ArrayList; import org.quartz.CronTrigger; import org.quartz.JobDetail; @@ -44,6 +48,15 @@ import org.quartz.impl.StdSchedulerFactory; import java.util.Date; +import java.util.List; +import org.quartz.CronScheduleBuilder; +import org.quartz.JobBuilder; +import org.quartz.JobKey; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import static org.quartz.TriggerKey.triggerKey; +import static org.quartz.impl.matchers.GroupMatcher.groupEquals; /** * JobSchedulerService @@ -72,6 +85,8 @@ public static JobSchedulerService getInstance( ) JobSchedulerService service = new JobSchedulerService( ); service.init( ); _singleton = service; + + loadTriggers( ); } } @@ -126,6 +141,154 @@ public Date scheduleJob( JobDetail job, CronTrigger trigger ) return date; } + /** + * Register a trigger + * + * @param daemonTrigger + * A daemon trigger representing the new trigger + * @return Date + */ + public Date createTrigger( DaemonTrigger daemonTrigger ) + { + Date date = null; + + String strTriggerKey = daemonTrigger.getKey( ); + String strTriggerGroup = daemonTrigger.getGroup( ); + String strCronExpression = daemonTrigger.getCronExpression( ); + String strDaemonKey = daemonTrigger.getDaemonKey( ); + + if ( _scheduler != null ) + { + try + { + JobDetail jobDetail; + + if ( !_scheduler.checkExists( new JobKey( "default", "default" ) ) ) + { + jobDetail = JobBuilder.newJob( DaemonJob.class ) + .withIdentity( "default", "default" ) + .storeDurably( ) + .build( ); + _scheduler.addJob( jobDetail, true ); + } + else + { + jobDetail = _scheduler.getJobDetail( new JobKey( "default", "default" ) ); + } + + CronTrigger cronTrigger = (CronTrigger) TriggerBuilder.newTrigger( ) + .withIdentity( strTriggerKey, strTriggerGroup ) + .withSchedule( CronScheduleBuilder.cronSchedule( strCronExpression ) ) + .usingJobData( "DaemonKey" , strDaemonKey ) + .forJob( jobDetail ) + .build( ); + + date = _scheduler.scheduleJob( cronTrigger ); + AppLogService.info( "New trigger registered : " + strTriggerKey + " - " + strTriggerGroup ); + } + catch( SchedulerException e ) + { + AppLogService.error( "Error registering trigger " + strTriggerKey + " - " + strTriggerGroup, e ); + } + } + + return date; + } + + /** + * Reregister a trigger + * + * @param oldDaemonTrigger + * A daemon trigger representing the old trigger + * @param newDaemonTrigger + * A daemon trigger representing the new trigger + * @return Date + */ + public Date updateTrigger( DaemonTrigger oldDaemonTrigger, DaemonTrigger newDaemonTrigger ) + { + Date date = null; + + String strOldTriggerKey = oldDaemonTrigger.getKey( ); + String strOldTriggerGroup = oldDaemonTrigger.getGroup( ); + String strTriggerKey = newDaemonTrigger.getKey( ); + String strTriggerGroup = newDaemonTrigger.getGroup( ); + String strCronExpression = newDaemonTrigger.getCronExpression( ); + String strDaemonKey = newDaemonTrigger.getDaemonKey( ); + + CronTrigger cronTrigger = (CronTrigger) getInstance( ).findTriggerByKey( strOldTriggerKey, strOldTriggerGroup ); + + TriggerBuilder triggerBuilder = cronTrigger.getTriggerBuilder( ); + + Trigger newTrigger = triggerBuilder.withIdentity( strTriggerKey, strTriggerGroup ) + .withSchedule( CronScheduleBuilder.cronSchedule( strCronExpression ) ) + .usingJobData( "DaemonKey", strDaemonKey ) + .build( ); + + if ( _scheduler != null ) + { + try + { + date = _scheduler.rescheduleJob( cronTrigger.getKey( ), newTrigger ); + AppLogService.info( "Trigger reregistered : " + strTriggerKey + " - " + strTriggerGroup ); + } + catch( SchedulerException e ) + { + AppLogService.error( "Error reregistering trigger " + strTriggerKey + " - " + strTriggerGroup, e ); + } + } + + return date; + } + + /** + * Unregister a trigger + * + * @param daemonTrigger + * A daemon trigger representing the trigger to remove + */ + public void removeTrigger( DaemonTrigger daemonTrigger ) + { + String strTriggerKey = daemonTrigger.getKey( ); + String strTriggerGroup = daemonTrigger.getGroup( ); + + if ( _scheduler != null ) + { + try + { + _scheduler.unscheduleJob( triggerKey( strTriggerKey, strTriggerGroup ) ); + AppLogService.info( "Trigger unregistered : " + strTriggerKey + " - " + strTriggerGroup ); + } + catch( SchedulerException e ) + { + AppLogService.error( "Error unregistering trigger " + strTriggerKey + " - " + strTriggerGroup, e ); + } + } + } + + /** + * Returns an instance of a trigger whose key is specified in parameter + * + * @param strTriggerKey + * The trigger key + * @param strTriggerGroup + * The trigger group + * @return the trigger + */ + public Trigger findTriggerByKey( String strTriggerKey, String strTriggerGroup ) + { + Trigger trigger = null; + try + { + trigger = _scheduler.getTrigger( triggerKey( strTriggerKey, strTriggerGroup ) ); + } + catch( SchedulerException e ) + { + AppLogService.error( "Error getting trigger " + strTriggerKey + " - " + strTriggerGroup, e ); + } + + return trigger; + } + /** * Shutdown the service (Called by the core while the webapp is destroyed) */ @@ -144,4 +307,44 @@ public static void shutdown( ) } } } + + /** + * get the trigger list + * + * @return the trigger list + */ + public static List getTriggersList( ) + { + List listTrigger = new ArrayList<>( ); + try + { + for( String triggerGroup : _scheduler.getTriggerGroupNames( ) ) + { + for( TriggerKey triggerKey : _scheduler.getTriggerKeys( groupEquals( triggerGroup ) ) ) + { + listTrigger.add( _scheduler.getTrigger( triggerKey ) ); + } + } + } + catch( SchedulerException e ) + { + AppLogService.error( "Error listing the triggers ", e ); + } + + return listTrigger; + } + + /** + * load the triggers save in database + * + */ + private static void loadTriggers( ) + { + List listDaemonTriggers = DaemonTriggerHome.getDaemonTriggersList( ); + + for ( DaemonTrigger daemonTrigger : listDaemonTriggers ) + { + getInstance( ).createTrigger( daemonTrigger ); + } + } } diff --git a/src/java/fr/paris/lutece/portal/web/system/TriggersJspBean.java b/src/java/fr/paris/lutece/portal/web/system/TriggersJspBean.java new file mode 100644 index 0000000000..3b5d8a2a20 --- /dev/null +++ b/src/java/fr/paris/lutece/portal/web/system/TriggersJspBean.java @@ -0,0 +1,379 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.web.system; + +import fr.paris.lutece.portal.business.daemon.DaemonTrigger; +import fr.paris.lutece.portal.business.daemon.DaemonTriggerHome; +import fr.paris.lutece.portal.service.admin.AccessDeniedException; +import fr.paris.lutece.portal.service.daemon.AppDaemonService; +import static fr.paris.lutece.portal.service.daemon.AppDaemonService.getDaemonEntries; +import fr.paris.lutece.portal.service.daemon.DaemonEntry; +import fr.paris.lutece.portal.service.i18n.I18nService; +import fr.paris.lutece.portal.service.message.AdminMessage; +import fr.paris.lutece.portal.service.message.AdminMessageService; +import fr.paris.lutece.portal.service.scheduler.JobSchedulerService; +import fr.paris.lutece.portal.service.security.SecurityTokenService; +import fr.paris.lutece.portal.service.util.AppPropertiesService; +import fr.paris.lutece.portal.util.mvc.admin.MVCAdminJspBean; +import fr.paris.lutece.portal.util.mvc.admin.annotations.Controller; +import fr.paris.lutece.portal.util.mvc.commons.annotations.Action; +import fr.paris.lutece.portal.util.mvc.commons.annotations.View; +import fr.paris.lutece.portal.web.util.LocalizedPaginator; +import fr.paris.lutece.util.ReferenceList; +import fr.paris.lutece.util.html.Paginator; +import fr.paris.lutece.util.url.UrlItem; +import java.util.Collection; + + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import org.quartz.CronExpression; + +/** + * + */ +@Controller( controllerJsp = "ManageTriggers.jsp", controllerPath = "jsp/admin/system/", right = "CORE_DAEMONS_MANAGEMENT" ) +public class TriggersJspBean extends MVCAdminJspBean +{ + // Templates + private static final String TEMPLATE_MANAGE_TRIGGERS = "admin/trigger/manage_triggers.html"; + private static final String TEMPLATE_CREATE_TRIGGER = "admin/trigger/create_trigger.html"; + private static final String TEMPLATE_MODIFY_TRIGGER = "admin/trigger/modify_trigger.html"; + + // Parameters + private static final String PARAMETER_DAEMON_TRIGGER_ID = "id"; + private static final String PARAMETER_TRIGGER_KEY = "key"; + private static final String PARAMETER_TRIGGER_GROUP = "group"; + private static final String PARAMETER_CRON_EXPRESSION = "cron_expression"; + private static final String PARAMETER_DAEMON_KEY = "daemon_key"; + + // Properties for page titles + private static final String PROPERTY_PAGE_TITLE_MANAGE_TRIGGERS = "portal.system.manage_triggers.pageTitle"; + private static final String PROPERTY_PAGE_TITLE_MODIFY_TRIGGER = "portal.system.modify_trigger.pageTitle"; + private static final String PROPERTY_PAGE_TITLE_CREATE_TRIGGER = "portal.system.create_trigger.pageTitle"; + + // Markers + private static final String MARK_DAEMON_TRIGGER_LIST = "daemon_triggers_list"; + private static final String MARK_DAEMON_TRIGGER = "daemon_trigger"; + private static final String MARK_DAEMONS_LIST = "daemons_list"; + private static final String MARK_PAGINATOR = "paginator"; + private static final String MARK_NB_ITEMS_PER_PAGE = "nb_items_per_page"; + + // JSP + private static final String JSP_MANAGE_TRIGGERS = "jsp/admin/system/ManageTriggers.jsp"; + + // Properties + private static final String MESSAGE_CONFIRM_REMOVE_TRIGGER = "portal.system.message.confirmRemoveTrigger"; + private static final String MESSAGE_TRIGGER_EXIST_ERROR = "portal.system.message.triggerExistsError"; + private static final String MESSAGE_INVALID_CRON_EXPRESSION_ERROR = "portal.system.message.invalidCronExpressionError"; + private static final String MESSAGE_INVALID_DAEMON_KEY_ERROR = "portal.system.message.invalidDaemonKeyError"; + private static final String PROPERTY_DEFAULT_LIST_ITEM_PER_PAGE = "daemon.daemontrigger.listItems.itemsPerPage"; + private static final String EMPTY_STRING = ""; + + // Validations + private static final String VALIDATION_ATTRIBUTES_PREFIX = "portal.system.model.entity.daemontrigger.attribute."; + + // Views + private static final String VIEW_MANAGE_TRIGGERS = "manageTriggers"; + private static final String VIEW_CREATE_TRIGGER = "createTrigger"; + private static final String VIEW_MODIFY_TRIGGER = "modifyTrigger"; + + // Actions + private static final String ACTION_CREATE_TRIGGER = "createTrigger"; + private static final String ACTION_MODIFY_TRIGGER = "modifyTrigger"; + private static final String ACTION_REMOVE_TRIGGER = "removeTrigger"; + private static final String ACTION_CONFIRM_REMOVE_TRIGGER = "confirmRemoveTrigger"; + + // Infos + private static final String INFO_TRIGGER_CREATED = "portal.system.job.info.trigger.created"; + private static final String INFO_TRIGGER_UPDATED = "portal.system.job.info.trigger.updated"; + private static final String INFO_TRIGGER_REMOVED = "portal.system.job.info.trigger.removed"; + + //Variables + private int _nDefaultItemsPerPage; + private String _strCurrentPageIndex; + private int _nItemsPerPage; + + + /** + * Build the Manage View + * @param request The HTTP request + * @return The page + */ + @View( value = VIEW_MANAGE_TRIGGERS, defaultView = true ) + public String getManageTriggers( HttpServletRequest request ) + { + _strCurrentPageIndex = Paginator.getPageIndex( request, Paginator.PARAMETER_PAGE_INDEX, _strCurrentPageIndex ); + _nDefaultItemsPerPage = AppPropertiesService.getPropertyInt( PROPERTY_DEFAULT_LIST_ITEM_PER_PAGE, 50 ); + _nItemsPerPage = Paginator.getItemsPerPage( request, Paginator.PARAMETER_ITEMS_PER_PAGE, _nItemsPerPage, _nDefaultItemsPerPage ); + + UrlItem url = new UrlItem( JSP_MANAGE_TRIGGERS ); + String strUrl = url.getUrl( ); + + List listDaemonTriggers = DaemonTriggerHome.getDaemonTriggersList( ); + + // PAGINATOR + LocalizedPaginator paginator = new LocalizedPaginator( listDaemonTriggers, _nItemsPerPage, strUrl, Paginator.PARAMETER_PAGE_INDEX, _strCurrentPageIndex, getLocale( ) ); + + Map model = new HashMap( ); + + model.put( MARK_NB_ITEMS_PER_PAGE, String.valueOf( _nItemsPerPage ) ); + model.put( MARK_PAGINATOR, paginator ); + model.put( MARK_DAEMON_TRIGGER_LIST, paginator.getPageItems( ) ); + + return getPage( PROPERTY_PAGE_TITLE_MANAGE_TRIGGERS, TEMPLATE_MANAGE_TRIGGERS, model ); + } + + /** + * Returns the form to create a trigger + * + * @param request The Http request + * @return the html code of the trigger form + */ + @View( VIEW_CREATE_TRIGGER ) + public String getCreateTrigger( HttpServletRequest request ) + { + Map model = new HashMap( ); + model.put( MARK_DAEMONS_LIST, getListDaemons( true ) ); + model.put( SecurityTokenService.MARK_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_CREATE_TRIGGER ) ); + return getPage( PROPERTY_PAGE_TITLE_CREATE_TRIGGER, TEMPLATE_CREATE_TRIGGER, model ); + } + + /** + * Process the data capture form of a new trigger + * + * @param request The Http Request + * @return The Jsp URL of the process result + * @throws AccessDeniedException + * if the security token is invalid + */ + @Action( ACTION_CREATE_TRIGGER ) + public String doCreateTrigger( HttpServletRequest request ) throws AccessDeniedException + { + String strTriggerKey = request.getParameter( PARAMETER_TRIGGER_KEY ); + String strTriggerGroup = request.getParameter( PARAMETER_TRIGGER_GROUP ); + String strCronExpression = request.getParameter( PARAMETER_CRON_EXPRESSION ); + String strDaemonKey = request.getParameter( PARAMETER_DAEMON_KEY ); + + DaemonTrigger daemonTrigger = new DaemonTrigger( ); + populate( daemonTrigger, request ); + + if ( !validateBean( daemonTrigger, VALIDATION_ATTRIBUTES_PREFIX ) ) + { + return redirect( request, VIEW_CREATE_TRIGGER ); + } + if ( JobSchedulerService.getInstance( ).findTriggerByKey( strTriggerKey, strTriggerGroup ) != null ) + { + return redirect( request, AdminMessageService.getMessageUrl( request, MESSAGE_TRIGGER_EXIST_ERROR, AdminMessage.TYPE_STOP ) ); + } + if ( !CronExpression.isValidExpression( strCronExpression ) ) + { + return redirect( request, AdminMessageService.getMessageUrl( request, MESSAGE_INVALID_CRON_EXPRESSION_ERROR, AdminMessage.TYPE_STOP ) ); + } + if ( !AppDaemonService.checkDaemonKey( strDaemonKey ) ) + { + return redirect( request, AdminMessageService.getMessageUrl( request, MESSAGE_INVALID_DAEMON_KEY_ERROR, AdminMessage.TYPE_STOP ) ); + } + assertSecurityToken( request, TEMPLATE_CREATE_TRIGGER ); + + JobSchedulerService.getInstance( ).createTrigger( daemonTrigger ); + DaemonTriggerHome.create( daemonTrigger ); + addInfo( INFO_TRIGGER_CREATED, getLocale( ) ); + + return redirectView( request, VIEW_MANAGE_TRIGGERS ); + } + + /** + * Manages the removal form of a trigger whose identifier is in the http + * request + * + * @param request The Http request + * @return the html code to confirm + */ + @Action( ACTION_CONFIRM_REMOVE_TRIGGER ) + public String getConfirmRemoveTrigger( HttpServletRequest request ) + { + String strDaemonTriggerKey = request.getParameter( PARAMETER_DAEMON_TRIGGER_ID ); + + UrlItem url = new UrlItem( getActionUrl( ACTION_REMOVE_TRIGGER ) ); + url.addParameter( PARAMETER_DAEMON_TRIGGER_ID, strDaemonTriggerKey ); + url.addParameter( SecurityTokenService.PARAMETER_TOKEN, SecurityTokenService.getInstance( ).getToken( request, JSP_MANAGE_TRIGGERS ) ); + + String strMessageUrl = AdminMessageService.getMessageUrl( request, MESSAGE_CONFIRM_REMOVE_TRIGGER, url.getUrl( ), AdminMessage.TYPE_CONFIRMATION ); + + return redirect( request, strMessageUrl ); + } + + /** + * Handles the removal form of a trigger + * + * @param request The Http request + * @return the jsp URL to display the form to manage triggers + * @throws AccessDeniedException + * if the security token is invalid + */ + @Action( ACTION_REMOVE_TRIGGER ) + public String doRemoveTrigger( HttpServletRequest request ) throws AccessDeniedException + { + String strDaemonTriggerId = request.getParameter( PARAMETER_DAEMON_TRIGGER_ID ); + int nDaemonTriggerId = Integer.parseInt( strDaemonTriggerId ); + + assertSecurityToken( request, JSP_MANAGE_TRIGGERS ); + + DaemonTrigger daemonTrigger = DaemonTriggerHome.findByPrimaryKey( nDaemonTriggerId ); + + JobSchedulerService.getInstance( ).removeTrigger( daemonTrigger ); + DaemonTriggerHome.remove( nDaemonTriggerId ); + addInfo( INFO_TRIGGER_REMOVED, getLocale( ) ); + + return redirectView( request, VIEW_MANAGE_TRIGGERS ); + } + + /** + * Returns the form to update info about a trigger + * + * @param request The Http request + * @return The HTML form to update info + */ + @View( VIEW_MODIFY_TRIGGER ) + public String getModifyTrigger( HttpServletRequest request ) + { + String strDaemonTriggerId = request.getParameter( PARAMETER_DAEMON_TRIGGER_ID ); + int nDaemonTriggerId = Integer.parseInt( strDaemonTriggerId ); + + DaemonTrigger daemonTrigger = DaemonTriggerHome.findByPrimaryKey( nDaemonTriggerId ); + + Map model = getModel( ); + model.put( MARK_DAEMON_TRIGGER, daemonTrigger ); + model.put( MARK_DAEMONS_LIST, getListDaemons( false ) ); + model.put( SecurityTokenService.MARK_TOKEN, SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MODIFY_TRIGGER ) ); + + return getPage( PROPERTY_PAGE_TITLE_MODIFY_TRIGGER, TEMPLATE_MODIFY_TRIGGER, model ); + } + + /** + * Process the change form of a trigger + * + * @param request The Http request + * @return The Jsp URL of the process result + * @throws AccessDeniedException + * if the security token is invalid + */ + @Action( ACTION_MODIFY_TRIGGER ) + public String doModifyTrigger( HttpServletRequest request ) throws AccessDeniedException + { + String strDaemonTriggerId = request.getParameter( PARAMETER_DAEMON_TRIGGER_ID ); + int nDaemonTriggerId = Integer.parseInt( strDaemonTriggerId ); + String strTriggerKey = request.getParameter( PARAMETER_TRIGGER_KEY ); + String strTriggerGroup = request.getParameter( PARAMETER_TRIGGER_GROUP ); + String strCronExpression = request.getParameter( PARAMETER_CRON_EXPRESSION ); + String strDaemonKey = request.getParameter( PARAMETER_DAEMON_KEY ); + + DaemonTrigger oldDaemonTrigger = DaemonTriggerHome.findByPrimaryKey( nDaemonTriggerId ); + DaemonTrigger newDaemonTrigger = new DaemonTrigger( ); + populate( newDaemonTrigger, request ); + + if ( !validateBean( newDaemonTrigger, VALIDATION_ATTRIBUTES_PREFIX ) ) + { + return redirect( request, VIEW_MODIFY_TRIGGER, PARAMETER_DAEMON_TRIGGER_ID, newDaemonTrigger.getId( ) ); + } + if ( !CronExpression.isValidExpression( strCronExpression ) ) + { + return redirect( request, AdminMessageService.getMessageUrl( request, MESSAGE_INVALID_CRON_EXPRESSION_ERROR, AdminMessage.TYPE_STOP ) ); + } + if ( !AppDaemonService.checkDaemonKey( strDaemonKey ) ) + { + return redirect( request, AdminMessageService.getMessageUrl( request, MESSAGE_INVALID_DAEMON_KEY_ERROR, AdminMessage.TYPE_STOP ) ); + } + if ( JobSchedulerService.getInstance( ).findTriggerByKey( strTriggerKey, strTriggerGroup ) != null + && ( !oldDaemonTrigger.getKey( ).equals( strTriggerKey ) || !oldDaemonTrigger.getGroup( ).equals( strTriggerGroup ) ) ) + { + return redirect( request, AdminMessageService.getMessageUrl( request, MESSAGE_TRIGGER_EXIST_ERROR, AdminMessage.TYPE_STOP ) ); + } + assertSecurityToken( request, TEMPLATE_MODIFY_TRIGGER ); + + JobSchedulerService.getInstance( ).updateTrigger( oldDaemonTrigger, newDaemonTrigger ); + DaemonTriggerHome.update( newDaemonTrigger ); + addInfo( INFO_TRIGGER_UPDATED, getLocale( ) ); + + return redirectView( request, VIEW_MANAGE_TRIGGERS ); + } + + /** + * Returns the list of daemons + * + * @param bEmptyString True to add an empty string in the list + * @return the list of daemons + */ + private ReferenceList getListDaemons( boolean bEmptyString ) + { + ReferenceList refList = new ReferenceList( ); + + if ( bEmptyString ) + { + // add empty item + refList.addItem( EMPTY_STRING, EMPTY_STRING ); + } + + Collection listDaemonEntries = getDaemonEntries( ); + + for ( DaemonEntry daemonEntry : listDaemonEntries ) + { + refList.addItem( daemonEntry.getId( ), I18nService.getLocalizedString( daemonEntry.getNameKey( ), getLocale( ) ) ); + } + + return refList; + } + + /** + * Asserts that the security token is valid + * + * @param request + * the request + * @param strTemplate + * the template of the page to access + * @throws AccessDeniedException + * if the security token is invalid + */ + private void assertSecurityToken( HttpServletRequest request, String strTemplate ) throws AccessDeniedException + { + if ( !SecurityTokenService.getInstance( ).validate( request, strTemplate ) ) + { + throw new AccessDeniedException( "Invalid security token" ); + } + } +} diff --git a/src/sql/create_db_lutece_core.sql b/src/sql/create_db_lutece_core.sql index c714e7da35..926b64b394 100644 --- a/src/sql/create_db_lutece_core.sql +++ b/src/sql/create_db_lutece_core.sql @@ -578,3 +578,16 @@ CREATE TABLE core_text_editor ( backOffice SMALLINT NOT NULL , PRIMARY KEY (editor_name, backOffice) ); + +-- +-- Table structure for table core_daemon_trigger +-- +DROP TABLE IF EXISTS core_daemon_trigger; +CREATE TABLE core_daemon_trigger ( + id_daemon_trigger int AUTO_INCREMENT, + trigger_key VARCHAR(50) DEFAULT '' NOT NULL, + trigger_group VARCHAR(50) DEFAULT '' NOT NULL, + cron_expression VARCHAR(50) DEFAULT '' NOT NULL, + daemon_key VARCHAR(50) DEFAULT '' NOT NULL, + PRIMARY KEY (id_daemon_trigger) +); diff --git a/src/sql/upgrade/update_db_lutece_core-6.1.1-6.1.2.sql b/src/sql/upgrade/update_db_lutece_core-6.1.1-6.1.2.sql new file mode 100644 index 0000000000..15071ef21a --- /dev/null +++ b/src/sql/upgrade/update_db_lutece_core-6.1.1-6.1.2.sql @@ -0,0 +1,12 @@ +-- +-- Table structure for table core_daemon_trigger +-- +DROP TABLE IF EXISTS core_daemon_trigger; +CREATE TABLE core_daemon_trigger ( + id_daemon_trigger int AUTO_INCREMENT, + key varchar(50) default '' NOT NULL, + group varchar(50) default '' NOT NULL, + cron_expression varchar(50) default '' NOT NULL, + daemon_key varchar(50) default '' NOT NULL, + PRIMARY KEY (id_daemon_trigger) +); \ No newline at end of file diff --git a/src/test/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHomeTest.java b/src/test/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHomeTest.java new file mode 100644 index 0000000000..d4e0fa1c74 --- /dev/null +++ b/src/test/java/fr/paris/lutece/portal/business/daemon/DaemonTriggerHomeTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.business.daemon; + +import fr.paris.lutece.test.LuteceTestCase; + +import java.util.List; + +public class DaemonTriggerHomeTest extends LuteceTestCase +{ + private final static String KEY_1 = "key1"; + private final static String KEY_2 = "key2"; + private final static String GROUP_1 = "group1"; + private final static String GROUP_2 = "group2"; + private final static String CRON_EXPRESSION_1 = "cronExpression1"; + private final static String CRON_EXPRESSION_2 = "cronExpression2"; + private final static String DAEMON_KEY_1 = "daemonKey1"; + private final static String DAEMON_KEY_2 = "daemonKey2"; + + public void testBusinessDaemonTrigger( ) + { + // Initialize an object + DaemonTrigger daemonTrigger = new DaemonTrigger( ); + daemonTrigger.setKey( KEY_1 ); + daemonTrigger.setGroup( GROUP_1 ); + daemonTrigger.setCronExpression( CRON_EXPRESSION_1 ); + daemonTrigger.setDaemonKey( DAEMON_KEY_1 ); + + // Create test + DaemonTriggerHome.create( daemonTrigger ); + + DaemonTrigger daemonTriggerStored = DaemonTriggerHome.findByPrimaryKey( daemonTrigger.getId( ) ); + assertEquals( daemonTriggerStored.getKey( ), daemonTrigger.getKey( ) ); + assertEquals( daemonTriggerStored.getGroup( ), daemonTrigger.getGroup( ) ); + assertEquals( daemonTriggerStored.getCronExpression( ), daemonTrigger.getCronExpression( ) ); + assertEquals( daemonTriggerStored.getDaemonKey( ), daemonTrigger.getDaemonKey( ) ); + + // Update test + daemonTrigger.setKey( KEY_2 ); + daemonTrigger.setGroup( GROUP_2 ); + daemonTrigger.setCronExpression( CRON_EXPRESSION_2 ); + daemonTrigger.setDaemonKey( DAEMON_KEY_2 ); + DaemonTriggerHome.update( daemonTrigger ); + + daemonTriggerStored = DaemonTriggerHome.findByPrimaryKey( daemonTrigger.getId( ) ); + assertEquals( daemonTriggerStored.getKey( ), daemonTrigger.getKey( ) ); + assertEquals( daemonTriggerStored.getGroup( ), daemonTrigger.getGroup( ) ); + assertEquals( daemonTriggerStored.getCronExpression( ), daemonTrigger.getCronExpression( ) ); + assertEquals( daemonTriggerStored.getDaemonKey( ), daemonTrigger.getDaemonKey( ) ); + + // List test + List listDaemonTriggers = DaemonTriggerHome.getDaemonTriggersList( ); + assertTrue( listDaemonTriggers.size( ) > 0 ); + + // Delete test + DaemonTriggerHome.remove( daemonTrigger.getId( ) ); + daemonTriggerStored = DaemonTriggerHome.findByPrimaryKey( daemonTrigger.getId( ) ); + assertNull( daemonTriggerStored ); + } +} diff --git a/src/test/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerServiceTest.java b/src/test/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerServiceTest.java new file mode 100644 index 0000000000..55a7bf5bb9 --- /dev/null +++ b/src/test/java/fr/paris/lutece/portal/service/scheduler/JobSchedulerServiceTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.service.scheduler; + +import fr.paris.lutece.portal.business.daemon.DaemonTrigger; +import fr.paris.lutece.portal.service.util.AppLogService; +import fr.paris.lutece.test.LuteceTestCase; + +import java.util.List; +import org.quartz.CronTrigger; + +public class JobSchedulerServiceTest extends LuteceTestCase +{ + private final static String KEY_1 = "key1"; + private final static String KEY_2 = "key2"; + private final static String GROUP_1 = "group1"; + private final static String GROUP_2 = "group2"; + private final static String CRON_EXPRESSION_1 = "0 0 0 * * ?"; + private final static String CRON_EXPRESSION_2 = "0 0 1 * * ?"; + private final static String DAEMON_KEY_1 = "daemonKey1"; + private final static String DAEMON_KEY_2 = "daemonKey2"; + + public void testServiceTrigger( ) + { + // Initialize an object + DaemonTrigger daemonTrigger = new DaemonTrigger( ); + daemonTrigger.setKey( KEY_1 ); + daemonTrigger.setGroup( GROUP_1 ); + daemonTrigger.setCronExpression( CRON_EXPRESSION_1 ); + daemonTrigger.setDaemonKey( DAEMON_KEY_1 ); + + // Create test + JobSchedulerService.getInstance( ).createTrigger( daemonTrigger ); + + CronTrigger triggerStored = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( daemonTrigger.getKey( ), daemonTrigger.getGroup( ) ); + assertEquals( triggerStored.getKey( ).getName( ), daemonTrigger.getKey( ) ); + assertEquals( triggerStored.getKey( ).getGroup( ), daemonTrigger.getGroup( ) ); + assertEquals( triggerStored.getCronExpression( ), daemonTrigger.getCronExpression( ) ); + assertEquals( triggerStored.getJobDataMap( ).get( "DaemonKey" ), daemonTrigger.getDaemonKey( ) ); + + // Update test + DaemonTrigger newDaemonTrigger = new DaemonTrigger( ); + newDaemonTrigger.setKey( KEY_2 ); + newDaemonTrigger.setGroup( GROUP_2 ); + newDaemonTrigger.setCronExpression( CRON_EXPRESSION_2 ); + newDaemonTrigger.setDaemonKey( DAEMON_KEY_2 ); + JobSchedulerService.getInstance( ).updateTrigger( daemonTrigger, newDaemonTrigger ); + + triggerStored = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( newDaemonTrigger.getKey( ), newDaemonTrigger.getGroup( ) ); + assertEquals( triggerStored.getKey( ).getName( ), newDaemonTrigger.getKey( ) ); + assertEquals( triggerStored.getKey( ).getGroup( ), newDaemonTrigger.getGroup( ) ); + assertEquals( triggerStored.getCronExpression( ), newDaemonTrigger.getCronExpression( ) ); + assertEquals( triggerStored.getJobDataMap( ).get( "DaemonKey" ), newDaemonTrigger.getDaemonKey( ) ); + + // List test + List listTriggers = JobSchedulerService.getInstance( ).getTriggersList( ); + assertTrue( listTriggers.size( ) > 0 ); + + // Delete test + JobSchedulerService.getInstance( ).removeTrigger( newDaemonTrigger ); + triggerStored = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( newDaemonTrigger.getKey( ), newDaemonTrigger.getGroup( ) ); + assertNull( triggerStored ); + } +} diff --git a/src/test/java/fr/paris/lutece/portal/web/system/TriggersJspBeanTest.java b/src/test/java/fr/paris/lutece/portal/web/system/TriggersJspBeanTest.java new file mode 100644 index 0000000000..d72e27cd09 --- /dev/null +++ b/src/test/java/fr/paris/lutece/portal/web/system/TriggersJspBeanTest.java @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2002-2018, Mairie de Paris + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice + * and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * License 1.0 + */ +package fr.paris.lutece.portal.web.system; + +import fr.paris.lutece.portal.business.daemon.DaemonTrigger; +import fr.paris.lutece.portal.business.daemon.DaemonTriggerHome; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.TimeoutException; + +import org.springframework.mock.web.MockHttpServletRequest; + +import fr.paris.lutece.portal.business.user.AdminUser; +import fr.paris.lutece.portal.service.admin.AccessDeniedException; +import fr.paris.lutece.portal.service.admin.PasswordResetException; +import fr.paris.lutece.portal.service.daemon.AppDaemonService; +import fr.paris.lutece.portal.service.daemon.DaemonEntry; +import fr.paris.lutece.portal.service.daemon.TestDaemon; +import fr.paris.lutece.portal.service.datastore.DatastoreService; +import fr.paris.lutece.portal.service.scheduler.JobSchedulerService; +import fr.paris.lutece.portal.service.security.SecurityTokenService; +import fr.paris.lutece.portal.service.util.AppLogService; +import fr.paris.lutece.test.LuteceTestCase; +import fr.paris.lutece.test.Utils; +import static junit.framework.TestCase.assertTrue; +import org.quartz.CronTrigger; +import org.springframework.mock.web.MockHttpServletResponse; + +public class TriggersJspBeanTest extends LuteceTestCase +{ + private static final String JUNIT_DAEMON = "JUNIT"; + private static final String JUNIT_DAEMON_2 = "JUNIT2"; + private static final String DAEMON_INTERVAL_DSKEY = "core.daemon." + JUNIT_DAEMON + ".interval"; + private TriggersJspBean bean; + private DaemonEntry _entry; + private TestDaemon _testDaemon; + private DaemonTrigger _oldDaemonTrigger; + + // Views + private static final String VIEW_MANAGE_TRIGGERS = "manageTriggers"; + private static final String VIEW_CREATE_TRIGGER = "createTrigger"; + private static final String VIEW_MODIFY_TRIGGER = "modifyTrigger"; + + // Actions + private static final String ACTION_CREATE_TRIGGER = "createTrigger"; + private static final String ACTION_MODIFY_TRIGGER = "modifyTrigger"; + private static final String ACTION_REMOVE_TRIGGER = "removeTrigger"; + private static final String ACTION_CONFIRM_REMOVE_TRIGGER = "confirmRemoveTrigger"; + + // Templates + private static final String TEMPLATE_CREATE_TRIGGER = "admin/trigger/create_trigger.html"; + private static final String TEMPLATE_MODIFY_TRIGGER = "admin/trigger/modify_trigger.html"; + + // JSP + private static final String JSP_MANAGE_TRIGGERS = "jsp/admin/system/ManageTriggers.jsp"; + + // Parameters + private static final String PARAMETER_DAEMON_TRIGGER_ID = "id"; + private static final String PARAMETER_TRIGGER_KEY = "key"; + private static final String PARAMETER_TRIGGER_GROUP = "group"; + private static final String PARAMETER_CRON_EXPRESSION = "cron_expression"; + private static final String PARAMETER_DAEMON_KEY = "daemon_key"; + + @Override + protected void setUp( ) throws Exception + { + super.setUp( ); + bean = new TriggersJspBean( ); + + _entry = new DaemonEntry( ); + _entry.setId( JUNIT_DAEMON ); + _entry.setNameKey( JUNIT_DAEMON ); + _entry.setDescriptionKey( JUNIT_DAEMON ); + _entry.setClassName( TestDaemon.class.getName( ) ); + _entry.setPluginName( "core" ); + // AppDaemonService.registerDaemon will copy this datastore value in the entry. + DatastoreService.setInstanceDataValue( DAEMON_INTERVAL_DSKEY, "1" ); + AppDaemonService.registerDaemon( _entry ); + _testDaemon = (TestDaemon) AppDaemonService.getDaemon( JUNIT_DAEMON ); + + _oldDaemonTrigger = new DaemonTrigger( ); + _oldDaemonTrigger.setKey( "test2" ); + _oldDaemonTrigger.setGroup( "test2" ); + _oldDaemonTrigger.setCronExpression( "0 0 0 * * ?" ); + _oldDaemonTrigger.setDaemonKey( JUNIT_DAEMON_2 ); + JobSchedulerService.getInstance( ).createTrigger( _oldDaemonTrigger ); + DaemonTriggerHome.create( _oldDaemonTrigger ); + } + + @Override + protected void tearDown( ) throws Exception + { + DatastoreService.removeInstanceData( DAEMON_INTERVAL_DSKEY ); + AppDaemonService.stopDaemon( JUNIT_DAEMON ); + AppDaemonService.unregisterDaemon( JUNIT_DAEMON ); + + _oldDaemonTrigger = DaemonTriggerHome.findByPrimaryKey( _oldDaemonTrigger.getId( ) ); + if ( _oldDaemonTrigger != null ) + { + DaemonTriggerHome.remove( _oldDaemonTrigger.getId( ) ); + JobSchedulerService.getInstance().removeTrigger( _oldDaemonTrigger ); + } + + super.tearDown( ); + } + + public void testGetManageTriggers( ) throws PasswordResetException, AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "view", VIEW_MANAGE_TRIGGERS ); + bean.init( request, DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + assertNotNull( bean.processController( request, response ) ); + } + + public void testGetCreateTrigger( ) throws PasswordResetException, AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "view", VIEW_CREATE_TRIGGER ); + bean.init( request, DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + assertNotNull( bean.processController( request, response ) ); + } + + public void testGetModifyTrigger( ) throws PasswordResetException, AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "view", VIEW_MODIFY_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + bean.init( request, DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + assertNotNull( bean.processController( request, response ) ); + } + + public void testGetConfirmRemoveTrigger( ) throws PasswordResetException, AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_CONFIRM_REMOVE_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + bean.init( request, DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + assertNull( bean.processController( request, response ) ); + } + + public void testDoCreateTrigger( ) + throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException + { + long lReadyTime; + long lScheduledTime; + + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_CREATE_TRIGGER ); + request.setParameter( PARAMETER_TRIGGER_KEY, "test" ); + request.setParameter( PARAMETER_TRIGGER_GROUP, "test" ); + request.setParameter( PARAMETER_CRON_EXPRESSION, "0/1 * * * * ?" ); + request.setParameter( PARAMETER_DAEMON_KEY, JUNIT_DAEMON ); + request.setParameter( SecurityTokenService.PARAMETER_TOKEN, + SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_CREATE_TRIGGER ) ); + + try + { + lReadyTime = System.nanoTime( ); + assertNull( bean.processController( request, response ) ); + + _testDaemon.go( ); + lScheduledTime = System.nanoTime( ); + AppLogService.info( "Daemon scheduled after " + ( ( lScheduledTime - lReadyTime ) / 1000000 ) + " ms" ); + _testDaemon.waitForCompletion( ); // Complete first run without a timeout + + lReadyTime = System.nanoTime( ); + _testDaemon.go( ); + lScheduledTime = System.nanoTime( ); + AppLogService.info( "Daemon scheduled after " + ( ( lScheduledTime - lReadyTime ) / 1000000 ) + " ms" ); + _testDaemon.waitForCompletion( ); // Complete second run without a timeout + + lReadyTime = System.nanoTime( ); + _testDaemon.go( ); + lScheduledTime = System.nanoTime( ); + long lRescheduleDurationMilliseconds = ( ( lScheduledTime - lReadyTime ) / 1000000 ); + AppLogService.info( "Daemon scheduled after " + ( ( lScheduledTime - lReadyTime ) / 1000000 ) + " ms" ); + _testDaemon.waitForCompletion( ); // Complete third run without a timeout. More runs would follow if we continued + + long lMarginMilliseconds = 250; + assertTrue( + "Daemon should be re-scheduled approximately 1 second after the end of the previous run, but got " + lRescheduleDurationMilliseconds + "ms", + 1000 - lMarginMilliseconds <= lRescheduleDurationMilliseconds && lRescheduleDurationMilliseconds <= 1000 + lMarginMilliseconds ); + + DaemonTrigger newDaemonTrigger = getDaemonTrigger( "test", "test" ); + assertNotNull( newDaemonTrigger ); + assertEquals( "0/1 * * * * ?", newDaemonTrigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON, newDaemonTrigger.getDaemonKey( ) ); + + CronTrigger trigger = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( "test", "test"); + assertNotNull( trigger ); + assertEquals( "0/1 * * * * ?", trigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON, trigger.getJobDataMap( ).get( "DaemonKey" ) ); + } + finally + { + DaemonTrigger newDaemonTrigger = getDaemonTrigger( "test", "test" ); + DaemonTriggerHome.remove( newDaemonTrigger.getId( ) ); + JobSchedulerService.getInstance().removeTrigger( newDaemonTrigger ); + } + } + + public void testDoCreateTriggerInvalidToken( ) + throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_CREATE_TRIGGER ); + request.setParameter( PARAMETER_TRIGGER_KEY, "test" ); + request.setParameter( PARAMETER_TRIGGER_GROUP, "test" ); + request.setParameter( PARAMETER_CRON_EXPRESSION, "0/1 * * * * ?" ); + request.setParameter( PARAMETER_DAEMON_KEY, JUNIT_DAEMON ); + request.setParameter( SecurityTokenService.PARAMETER_TOKEN, + SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_CREATE_TRIGGER + 'b' ) ); + + try + { + bean.processController( request, response ); + fail( "Should have thrown" ); + } + catch( AccessDeniedException e ) + { + DaemonTrigger newDaemonTrigger = getDaemonTrigger( "test", "test" ); + assertNull( newDaemonTrigger ); + + CronTrigger trigger = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( "test", "test"); + assertNull( trigger ); + } + } + + public void testDoCreateTriggerNoToken( ) + throws InterruptedException, BrokenBarrierException, TimeoutException, AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_CREATE_TRIGGER ); + request.setParameter( PARAMETER_TRIGGER_KEY, "test" ); + request.setParameter( PARAMETER_TRIGGER_GROUP, "test" ); + request.setParameter( PARAMETER_CRON_EXPRESSION, "0/1 * * * * ?" ); + request.setParameter( PARAMETER_DAEMON_KEY, JUNIT_DAEMON ); + + try + { + bean.processController( request, response ); + fail( "Should have thrown" ); + } + catch( AccessDeniedException e ) + { + DaemonTrigger newDaemonTrigger = getDaemonTrigger( "test", "test" ); + assertNull( newDaemonTrigger ); + + CronTrigger trigger = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( "test", "test"); + assertNull( trigger ); + } + } + + public void testDoModifyTrigger( ) throws AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_MODIFY_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + request.setParameter( PARAMETER_TRIGGER_KEY, "test" ); + request.setParameter( PARAMETER_TRIGGER_GROUP, "test" ); + request.setParameter( PARAMETER_CRON_EXPRESSION, "0/1 * * * * ?" ); + request.setParameter( PARAMETER_DAEMON_KEY, JUNIT_DAEMON ); + request.setParameter( SecurityTokenService.PARAMETER_TOKEN, + SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MODIFY_TRIGGER ) ); + + try + { + assertNull( bean.processController( request, response ) ); + } catch (Throwable t) + { + AppLogService.error( "Error with test ", t ); + } + + DaemonTrigger newDaemonTrigger = DaemonTriggerHome.findByPrimaryKey( _oldDaemonTrigger.getId( ) ); + assertNotNull( newDaemonTrigger ); + assertEquals( "test", newDaemonTrigger.getKey( ) ); + assertEquals( "test", newDaemonTrigger.getGroup( ) ); + assertEquals( "0/1 * * * * ?", newDaemonTrigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON, newDaemonTrigger.getDaemonKey( ) ); + + CronTrigger trigger = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( "test", "test"); + assertNotNull( trigger ); + assertEquals( "0/1 * * * * ?", trigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON, trigger.getJobDataMap( ).get( "DaemonKey" ) ); + } + + public void testDoModifyTriggerInvalidToken( ) throws AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_MODIFY_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + request.setParameter( PARAMETER_TRIGGER_KEY, "test" ); + request.setParameter( PARAMETER_TRIGGER_GROUP, "test" ); + request.setParameter( PARAMETER_CRON_EXPRESSION, "0/1 * * * * ?" ); + request.setParameter( PARAMETER_DAEMON_KEY, JUNIT_DAEMON ); + request.setParameter( SecurityTokenService.PARAMETER_TOKEN, + SecurityTokenService.getInstance( ).getToken( request, TEMPLATE_MODIFY_TRIGGER + 'b' ) ); + + try + { + bean.processController( request, response ); + fail( "Should have thrown" ); + } + catch( AccessDeniedException e ) + { + DaemonTrigger newDaemonTrigger = DaemonTriggerHome.findByPrimaryKey( _oldDaemonTrigger.getId( ) ); + assertNotNull( newDaemonTrigger ); + assertEquals( "test2", newDaemonTrigger.getKey( ) ); + assertEquals( "test2", newDaemonTrigger.getGroup( ) ); + assertEquals( "0 0 0 * * ?", newDaemonTrigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON_2, newDaemonTrigger.getDaemonKey( ) ); + + CronTrigger trigger = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( "test2", "test2"); + assertNotNull( trigger ); + assertEquals( "0 0 0 * * ?", trigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON_2, trigger.getJobDataMap( ).get( "DaemonKey" ) ); + } + } + + public void testDoModifyTriggerNoToken( ) throws AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_MODIFY_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + request.setParameter( PARAMETER_TRIGGER_KEY, "test" ); + request.setParameter( PARAMETER_TRIGGER_GROUP, "test" ); + request.setParameter( PARAMETER_CRON_EXPRESSION, "0/1 * * * * ?" ); + request.setParameter( PARAMETER_DAEMON_KEY, JUNIT_DAEMON ); + + try + { + bean.processController( request, response ); + fail( "Should have thrown" ); + } + catch( AccessDeniedException e ) + { + DaemonTrigger newDaemonTrigger = DaemonTriggerHome.findByPrimaryKey( _oldDaemonTrigger.getId( ) ); + assertNotNull( newDaemonTrigger ); + assertEquals( "test2", newDaemonTrigger.getKey( ) ); + assertEquals( "test2", newDaemonTrigger.getGroup( ) ); + assertEquals( "0 0 0 * * ?", newDaemonTrigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON_2, newDaemonTrigger.getDaemonKey( ) ); + + CronTrigger trigger = (CronTrigger) JobSchedulerService.getInstance( ).findTriggerByKey( "test2", "test2"); + assertNotNull( trigger ); + assertEquals( "0 0 0 * * ?", trigger.getCronExpression( ) ); + assertEquals( JUNIT_DAEMON_2, trigger.getJobDataMap( ).get( "DaemonKey" ) ); + } + } + + public void testDoRemoveTrigger( ) throws AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_REMOVE_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + request.setParameter( SecurityTokenService.PARAMETER_TOKEN, + SecurityTokenService.getInstance( ).getToken( request, JSP_MANAGE_TRIGGERS ) ); + assertNull( bean.processController( request, response ) ); + assertNull( DaemonTriggerHome.findByPrimaryKey( _oldDaemonTrigger.getId( ) ) ); + } + + public void testDoRemoveTriggerInvalidToken( ) throws AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_REMOVE_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + request.setParameter( SecurityTokenService.PARAMETER_TOKEN, + SecurityTokenService.getInstance( ).getToken( request, JSP_MANAGE_TRIGGERS + 'b' ) ); + + try + { + bean.processController( request, response ); + fail( "Should have thrown" ); + } + catch( AccessDeniedException e ) + { + assertNotNull( DaemonTriggerHome.findByPrimaryKey( _oldDaemonTrigger.getId( ) ) ); + } + } + + public void testDoRemoveTriggerNoToken( ) throws AccessDeniedException + { + MockHttpServletRequest request = new MockHttpServletRequest( ); + MockHttpServletResponse response = new MockHttpServletResponse( ); + Utils.registerAdminUserWithRigth( request, new AdminUser( ), DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); + request.setParameter( "action", ACTION_REMOVE_TRIGGER ); + request.setParameter( PARAMETER_DAEMON_TRIGGER_ID, String.valueOf( _oldDaemonTrigger.getId( ) ) ); + + try + { + bean.processController( request, response ); + fail( "Should have thrown" ); + } + catch( AccessDeniedException e ) + { + assertNotNull( DaemonTriggerHome.findByPrimaryKey( _oldDaemonTrigger.getId( ) ) ); + } + } + + private DaemonTrigger getDaemonTrigger( String strTriggerKey, String strTriggerGroup ) + { + DaemonTrigger daemonTrigger = null; + for (DaemonTrigger dt : DaemonTriggerHome.getDaemonTriggersList( ) ) + { + if ( dt.getKey( ).equals( strTriggerKey ) && dt.getGroup( ).equals( strTriggerGroup ) ) + { + daemonTrigger = dt; + } + } + + return daemonTrigger; + } +} diff --git a/webapp/WEB-INF/conf/core_context.xml b/webapp/WEB-INF/conf/core_context.xml index cf9e7421b0..7363c2dad6 100644 --- a/webapp/WEB-INF/conf/core_context.xml +++ b/webapp/WEB-INF/conf/core_context.xml @@ -226,4 +226,6 @@ + + diff --git a/webapp/WEB-INF/conf/daemons.properties b/webapp/WEB-INF/conf/daemons.properties index 120a691e14..599d7430c1 100644 --- a/webapp/WEB-INF/conf/daemons.properties +++ b/webapp/WEB-INF/conf/daemons.properties @@ -42,3 +42,9 @@ daemon.accountLifeTimeDaemon.onstartup=1 daemon.threadLauncherDaemon.interval=86400 daemon.threadLauncherDaemon.onstartup=1 daemon.threadLauncherDaemon.maxNumberOfThread=10 + +################################################################################ +# daemon triggers parameters + +# pagination options +daemon.daemontrigger.listItems.itemsPerPage=50 \ No newline at end of file diff --git a/webapp/WEB-INF/templates/admin/system/manage_daemons.html b/webapp/WEB-INF/templates/admin/system/manage_daemons.html index f83646b20d..7080d32680 100644 --- a/webapp/WEB-INF/templates/admin/system/manage_daemons.html +++ b/webapp/WEB-INF/templates/admin/system/manage_daemons.html @@ -1,7 +1,11 @@ <@row> <@columns> <@box color='danger'> - <@boxHeader title='#i18n{portal.system.manage_daemons.titleCacheList}' /> + <@boxHeader title='#i18n{portal.system.manage_daemons.titleCacheList}' > + <@tform class='form-inline pull-right' name='manage_trigger' action='jsp/admin/system/ManageTriggers.jsp'> + <@button type='submit' name='view_manageTrigger' buttonIcon='cogs' title='#i18n{portal.system.manage_daemons.buttonManageTriggers}' /> + + <@boxBody> <@table> diff --git a/webapp/WEB-INF/templates/admin/trigger/create_trigger.html b/webapp/WEB-INF/templates/admin/trigger/create_trigger.html new file mode 100644 index 0000000000..f51861b273 --- /dev/null +++ b/webapp/WEB-INF/templates/admin/trigger/create_trigger.html @@ -0,0 +1,27 @@ +<@row> + <@columns> + <@box color='danger'> + <@boxHeader title='#i18n{portal.system.create_trigger.title}' /> + <@boxBody> + <@tform name='create_trigger' action='jsp/admin/system/ManageTriggers.jsp'> + <@messages errors=errors /> + + <@formGroup labelKey='#i18n{portal.system.create_trigger.labelKey}' helpKey='#i18n{portal.system.create_trigger.labelKey.help}' mandatory=true> + <@input type='text' name='key' value='' /> + + <@formGroup labelKey='#i18n{portal.system.create_trigger.labelGroup}' helpKey='#i18n{portal.system.create_trigger.labelGroup.help}' mandatory=true> + <@input type='text' name='group' value='' /> + + <@formGroup labelKey='#i18n{portal.system.create_trigger.labelCronExpression}' helpKey='#i18n{portal.system.create_trigger.labelCronExpression.help}' mandatory=true> + <@input type='text' name='cron_expression' value='' /> + + <@formGroup labelKey='#i18n{portal.system.create_trigger.labelDaemonKey}' helpKey='#i18n{portal.system.create_trigger.labelDaemonKey.help}' mandatory=true> + <@select name='daemon_key' items=daemons_list default_value='' /> + + + <@actionButtons button1Name="action_createTrigger" button2Name="view_manageTrigger"/> + + + + + \ No newline at end of file diff --git a/webapp/WEB-INF/templates/admin/trigger/manage_triggers.html b/webapp/WEB-INF/templates/admin/trigger/manage_triggers.html new file mode 100644 index 0000000000..35c498e2fb --- /dev/null +++ b/webapp/WEB-INF/templates/admin/trigger/manage_triggers.html @@ -0,0 +1,53 @@ +<@row> + <@columns> + <@box color='danger'> + <@boxHeader title='#i18n{portal.system.manage_triggers.title}' boxTools=true> + <@tform class='form-inline pull-right' name='manage_trigger' action='jsp/admin/system/ManageTriggers.jsp'> + <@button type='submit' name='view_createTrigger' buttonIcon='plus' title='#i18n{portal.system.manage_triggers.buttonAdd}' /> + + <@aButton size='' href='jsp/admin/system/ManageDaemons.jsp' title='#i18n{portal.system.manage_triggers.buttonManageDaemons}' buttonIcon='cogs' showTitleXs=false showTitleSm=false /> + + <@boxBody> + <@messages infos=infos /> + <@paginationAdmin paginator=paginator combo=1 /> +
+ <@table> + + + #i18n{portal.system.manage_triggers.columnKey} + #i18n{portal.system.manage_triggers.columnGroup} + #i18n{portal.system.manage_triggers.columnCronExpression} + #i18n{portal.system.manage_triggers.columnDaemonKey} + #i18n{portal.util.labelActions} + + <@tableHeadBodySeparator /> + <#list daemon_triggers_list as daemon_trigger > + + + ${daemon_trigger.key} + + + ${daemon_trigger.group} + + + ${daemon_trigger.cronExpression} + + + ${daemon_trigger.daemonKey} + + + <@aButton href='jsp/admin/system/ManageTriggers.jsp?view=modifyTrigger&id=${daemon_trigger.id}' title='#i18n{portal.util.labelModify}' buttonIcon='pencil' showTitle=false /> + + <@aButton href='jsp/admin/system/ManageTriggers.jsp?action=confirmRemoveTrigger&id=${daemon_trigger.id}' title='#i18n{portal.util.labelDelete}' buttonIcon='trash' color='btn-danger' showTitle=false /> + + + + + + + <@paginationAdmin paginator=paginator /> + + + + + \ No newline at end of file diff --git a/webapp/WEB-INF/templates/admin/trigger/modify_trigger.html b/webapp/WEB-INF/templates/admin/trigger/modify_trigger.html new file mode 100644 index 0000000000..11c83d48a7 --- /dev/null +++ b/webapp/WEB-INF/templates/admin/trigger/modify_trigger.html @@ -0,0 +1,27 @@ +<@row> + <@columns> + <@box color='danger'> + <@boxHeader title='#i18n{portal.system.modify_trigger.title}' /> + <@boxBody> + <@tform class="form-horizontal" method="post" name="modify_trigger" action="jsp/admin/system/ManageTriggers.jsp"> + <@messages errors=errors /> + + + <@formGroup labelKey='#i18n{portal.system.modify_trigger.labelKey}' helpKey='#i18n{portal.system.modify_trigger.labelKey.help}' mandatory=true> + <@input type='text' name='key' value='${daemon_trigger.key}' /> + + <@formGroup labelKey='#i18n{portal.system.modify_trigger.labelGroup}' helpKey='#i18n{portal.system.modify_trigger.labelGroup.help}' mandatory=true> + <@input type='text' name='group' value='${daemon_trigger.group}' /> + + <@formGroup labelKey='#i18n{portal.system.modify_trigger.labelCronExpression}' helpKey='#i18n{portal.system.modify_trigger.labelCronExpression.help}' mandatory=true> + <@input type='text' name='cron_expression' value='${daemon_trigger.cronExpression}' /> + + <@formGroup labelKey='#i18n{portal.system.modify_trigger.labelDaemonKey}' helpKey='#i18n{portal.system.modify_trigger.labelDaemonKey.help}' mandatory=true> + <@select name='daemon_key' items=daemons_list default_value='${daemon_trigger.daemonKey}' /> + + <@actionButtons button1Name="action_modifyTrigger" button2Name="view_manageTrigger"/> + + + + + \ No newline at end of file diff --git a/webapp/jsp/admin/system/ManageTriggers.jsp b/webapp/jsp/admin/system/ManageTriggers.jsp new file mode 100644 index 0000000000..cd3fba05c7 --- /dev/null +++ b/webapp/jsp/admin/system/ManageTriggers.jsp @@ -0,0 +1,10 @@ + +<% triggers.init( request , fr.paris.lutece.portal.web.system.DaemonsJspBean.RIGHT_DAEMONS_MANAGEMENT ); %> +<% String strContent = triggers.processController ( request , response ); %> + +<%@ page errorPage="../ErrorPage.jsp" %> + + +<%= strContent %> + +<%@ include file="../AdminFooter.jsp" %>