NotificationService.java
/*
* Copyright (c) 2002-2023, 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.plugins.notificationstore.service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.paris.lutece.plugins.grubusiness.business.customer.Customer;
import fr.paris.lutece.plugins.grubusiness.business.demand.Demand;
import fr.paris.lutece.plugins.grubusiness.business.demand.DemandStatus;
import fr.paris.lutece.plugins.grubusiness.business.demand.IDemandServiceProvider;
import fr.paris.lutece.plugins.grubusiness.business.notification.Event;
import fr.paris.lutece.plugins.grubusiness.business.notification.Notification;
import fr.paris.lutece.plugins.grubusiness.business.notification.NotificationEvent;
import fr.paris.lutece.plugins.grubusiness.business.notification.StatusMessage;
import fr.paris.lutece.plugins.grubusiness.business.web.rs.EnumGenericStatus;
import fr.paris.lutece.plugins.grubusiness.service.notification.INotifyerServiceProvider;
import fr.paris.lutece.plugins.grubusiness.service.notification.NotificationException;
import fr.paris.lutece.plugins.identitystore.web.exception.IdentityNotFoundException;
import fr.paris.lutece.plugins.identitystore.web.exception.IdentityStoreException;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.util.AppLogService;
public class NotificationService
{
// Bean names
private static final String BEAN_STORAGE_SERVICE = "notificationstore.demandService";
// Other constants
private static final String RESPONSE_OK = "{ \"acknowledge\" : { \"status\": \"received\" } }";
private static final String TYPE_DEMAND = "DEMAND";
private static final String TYPE_NOTIFICATION = "NOTIFICATION";
private static final String STATUS_WARNING = "WARNING";
private static final String STATUS_ERROR = "ERROR";
private static final String STATUS_FAILED = "FAILED";
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String TYPE_GUICHET = "GUICHET";
// Messages
private static final String WARNING_DEMAND_ID_MANDATORY = "Notification Demand_id field is mandatory";
private static final String WARNING_DEMAND_TYPE_ID_MANDATORY = "Notification Demand_type_id field is mandatory";
private static final String WARNING_CUSTOMER_ID_MANDATORY = "Notification connection_id field is mandatory";
private static final String MESSAGE_MISSING_MANDATORY_FIELD = "Missing value";
private static final String MESSAGE_MISSING_DEMAND_ID = "Demand Id and Demand type Id are mandatory";
private static final String MESSAGE_MISSING_USER_ID = "User connection id is mandatory";
private static final String MESSAGE_INCORRECT_DEMAND_ID = "Demand Type Id not found";
// instance variables
private static IDemandServiceProvider _demandService;
private static NotificationService _instance;
private static List<INotifyerServiceProvider> _notifyers ;
/**
* private constructor
*/
private NotificationService( )
{
}
/**
* get unique instance of the service
* @return the notification service
*/
public static NotificationService instance( )
{
if ( _instance == null )
{
_instance = new NotificationService( );
_demandService = SpringContextService.getBean( BEAN_STORAGE_SERVICE );
_notifyers = SpringContextService.getBeansOfType( INotifyerServiceProvider.class ) ;
}
return _instance;
}
/**
* process Notification
*
* @param notification
*/
public Response newNotification(String strJson)
{
List<StatusMessage> warnings = new ArrayList<>( );
try
{
// parse json
Notification notification = getNotificationFromJson( strJson );
// control customer
processCustomer( notification );
// store any notification whatever its content
store( notification );
// check Notification
checkNotification( notification, warnings );
// create Event if a MyDashboard type of notification exists
if ( notification.getMyDashboardNotification( ) != null )
{
addMydashboardNotificationEvent( notification );
}
// forward notification to registred notifyers (if exists)
forward( notification );
}
catch( JsonParseException ex )
{
return fail( ex, Response.Status.BAD_REQUEST );
}
catch( JsonMappingException | NullPointerException | NotificationException ex )
{
return fail( ex, Response.Status.BAD_REQUEST );
}
catch( IOException ex )
{
return fail( ex, Response.Status.INTERNAL_SERVER_ERROR );
}
catch( Exception ex )
{
return fail( ex, Response.Status.INTERNAL_SERVER_ERROR );
}
// success
if ( warnings.isEmpty( ) )
{
return success( );
}
else
{
return successWithWarnings( warnings );
}
}
/**
* Notification check
*
* @param notification
* @param warnings
*/
private void checkNotification(Notification notification, List<StatusMessage> warnings) {
// notification should be associated to a demand id
if ( StringUtils.isBlank( notification.getDemand( ).getId( ) ) )
{
StatusMessage msg = new StatusMessage( TYPE_DEMAND, STATUS_WARNING, MESSAGE_MISSING_MANDATORY_FIELD, WARNING_DEMAND_ID_MANDATORY );
warnings.add( msg );
}
// notification should be associated to a demand type id
if ( StringUtils.isBlank( notification.getDemand( ).getTypeId( ) ) )
{
StatusMessage msg = new StatusMessage( TYPE_DEMAND, STATUS_WARNING, MESSAGE_MISSING_MANDATORY_FIELD, WARNING_DEMAND_TYPE_ID_MANDATORY );
warnings.add( msg );
}
// notification should be associated to a customer id
if ( notification.getDemand( ).getCustomer( ) != null
&& StringUtils.isBlank( notification.getDemand( ).getCustomer( ).getConnectionId( ) ) )
{
StatusMessage msg = new StatusMessage( TYPE_DEMAND, STATUS_WARNING, MESSAGE_MISSING_MANDATORY_FIELD, WARNING_CUSTOMER_ID_MANDATORY );
warnings.add( msg );
}
}
/**
*
* @param notification
* @throws IdentityStoreException
*/
private void processCustomer(Notification notification) throws IdentityStoreException {
Customer customerEncrypted = notification.getDemand( ).getCustomer( );
if ( CustomerProvider.instance( ).hasIdentityService( ) )
{
Customer customerDecrypted = CustomerProvider.instance( ).decrypt( customerEncrypted, notification.getDemand( ) );
if ( customerDecrypted != null && StringUtils.isNotEmpty( customerDecrypted.getConnectionId( ) )
&& StringUtils.isEmpty( customerDecrypted.getId( ) ) )
{
try
{
Customer customerTmp = CustomerProvider.instance( ).get( customerDecrypted.getConnectionId( ), StringUtils.EMPTY );
customerDecrypted.setId( customerTmp.getId( ) );
}
catch ( IdentityNotFoundException e )
{
// customer not found in IDS
AppLogService.debug( "Customer not found with connection_id : " + customerDecrypted.getConnectionId( ) );
customerDecrypted = null;
}
}
if ( customerDecrypted == null )
{
customerDecrypted = new Customer( );
customerDecrypted.setConnectionId( StringUtils.EMPTY );
customerDecrypted.setId( StringUtils.EMPTY );
notification.getDemand().setCustomer( customerDecrypted );
}
notification.getDemand( ).setCustomer( customerDecrypted );
}
else
{
notification.getDemand( ).setCustomer( customerEncrypted );
}
}
/**
* store a notification event
*
* @param strJson
* @return the response
*/
public Response newNotificationEvent(String strJson)
{
try
{
// Format from JSON
ObjectMapper mapper = new ObjectMapper( );
mapper.configure( DeserializationFeature.UNWRAP_ROOT_VALUE, true );
mapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
NotificationEvent notificationEvent = mapper.readValue( strJson, NotificationEvent.class );
AppLogService.debug( "notificationstore / notificationEvent - Received strJson : " + strJson );
store( notificationEvent );
}
catch( JsonParseException ex )
{
return fail( ex, Response.Status.BAD_REQUEST );
}
catch( JsonMappingException | NullPointerException ex )
{
return fail( ex, Response.Status.BAD_REQUEST );
}
catch( IOException ex )
{
return fail( ex, Response.Status.INTERNAL_SERVER_ERROR );
}
catch( Exception ex )
{
return fail( ex, Response.Status.INTERNAL_SERVER_ERROR );
}
return success( );
}
/**
* Stores a notification and the associated demand
*
* @param notification
* the notification to store
*/
private void store( NotificationEvent notificationEvent )
{
_demandService.create( notificationEvent );
}
/**
* Stores a notification and the associated demand
*
* @param notification
* the notification to store
*/
private void store( Notification notification )
{
Demand demand = _demandService.findByPrimaryKey( notification.getDemand( ).getId( ), notification.getDemand( ).getTypeId( ) );
if ( demand == null ||
( demand.getCustomer( ) != null && demand.getCustomer( ).getId( ) != null
&& !demand.getCustomer( ).getId( ).equals( notification.getDemand( ).getCustomer( ).getId( ) ) ) )
{
demand = new Demand( );
demand.setId( notification.getDemand( ).getId( ) );
demand.setTypeId( notification.getDemand( ).getTypeId( ) );
demand.setSubtypeId( notification.getDemand( ).getSubtypeId( ) );
demand.setReference( notification.getDemand( ).getReference( ) );
demand.setCreationDate( notification.getDate( ) );
demand.setMaxSteps( notification.getDemand( ).getMaxSteps( ) );
demand.setCurrentStep( notification.getDemand( ).getCurrentStep( ) );
demand.setStatusId( getNewDemandStatusIdFromNotification( notification ) );
Customer customerDemand = new Customer( );
customerDemand.setId( notification.getDemand( ).getCustomer( ).getId( ) );
customerDemand.setConnectionId( notification.getDemand( ).getCustomer( ).getConnectionId( ) );
demand.setCustomer( customerDemand );
// create demand
_demandService.create( demand );
}
else
{
demand.getCustomer( ).setId( notification.getDemand( ).getCustomer( ).getId( ) );
if( StringUtils.isEmpty( demand.getCustomer( ).getConnectionId( ) ) )
{
demand.getCustomer( ).setConnectionId( notification.getDemand( ).getCustomer( ).getConnectionId( ) );
}
demand.setCurrentStep( notification.getDemand( ).getCurrentStep( ) );
int nNewStatusId = getNewDemandStatusIdFromNotification( notification );
EnumGenericStatus oldStatus = EnumGenericStatus.getByStatusId( demand.getStatusId( ) );
EnumGenericStatus newStatus = EnumGenericStatus.getByStatusId( nNewStatusId );
// Demand opened to closed
if ( oldStatus != null && newStatus != null
&& !oldStatus.isFinalStatus( ) && newStatus.isFinalStatus( ) )
{
demand.setClosureDate( notification.getDate( ) );
}
// Demand closed to opened
if ( oldStatus != null && newStatus != null
&& oldStatus.isFinalStatus( ) && !newStatus.isFinalStatus( ) )
{
demand.setClosureDate( 0 );
}
_demandService.update( demand );
}
notification.setDemand( demand );
// create notification
_demandService.create( notification );
}
/**
* Check notification
* @param notif
* @return The message error
*/
private String checkNotification( Notification notif )
{
// check if connection id is present
if ( notif.getDemand( ) == null || notif.getDemand( ).getCustomer( ) == null || StringUtils.isBlank( notif.getDemand( ).getCustomer( ).getConnectionId( ) ) )
{
return generateErrorMessage( notif, Response.Status.PRECONDITION_FAILED, MESSAGE_MISSING_USER_ID );
}
// check if Demand remote id and demand type id are present
if ( StringUtils.isBlank( notif.getDemand( ).getId( ) ) || StringUtils.isBlank( notif.getDemand( ).getTypeId( ) ) )
{
return generateErrorMessage( notif, Response.Status.PRECONDITION_FAILED, MESSAGE_MISSING_DEMAND_ID );
}
// check id demand_type_id is numeric
if ( !StringUtils.isNumeric( notif.getDemand( ).getTypeId( ) ) )
{
return generateErrorMessage( notif, Response.Status.PRECONDITION_FAILED, MESSAGE_INCORRECT_DEMAND_ID );
}
// check if demand type id exists
if ( !_demandService.getDemandType( Integer.parseInt( notif.getDemand( ).getTypeId( ) ) ).isPresent( ) )
{
return generateErrorMessage( notif, Response.Status.PRECONDITION_FAILED, MESSAGE_INCORRECT_DEMAND_ID );
}
return StringUtils.EMPTY;
}
/**
* Values and store the NotificationEvent object if failure
* @param notification
* @param strMessage
*/
private void addMydashboardNotificationEvent ( Notification notification )
{
if ( notification.getMyDashboardNotification( ) != null )
{
Event event = new Event( );
event.setType( TYPE_GUICHET );
event.setEventDate( notification.getDate( ) );
String strMessage = checkNotification( notification );
if( StringUtils.isNotEmpty( strMessage ))
{
event.setMessage( strMessage );
event.setStatus( STATUS_FAILED );
}
else
{
event.setStatus( STATUS_SUCCESS );
}
NotificationEvent notificationEvent = new NotificationEvent( );
notificationEvent.setEvent( event );
notificationEvent.setMsgId( StringUtils.EMPTY );
notificationEvent.setDemand( notification.getDemand( ) );
notificationEvent.setNotificationDate( notification.getDate( ) );
store( notificationEvent );
}
}
/**
* Build an error response
*
* @param strMessage
* The error message
* @param ex
* An exception
* @return The response
*/
private Response successWithWarnings( List<StatusMessage> warnings )
{
StringBuilder strWarnings = new StringBuilder( "[" );
if (warnings != null)
{
for ( StatusMessage msg : warnings )
{
strWarnings.append( msg.asJson( ) ).append( "," );
}
// remove last ","
strWarnings.setLength( strWarnings.length( ) - 1);
}
strWarnings.append( "]" );
String strResponse = "{ \"acknowledge\" : { \"status\": \"warning\", \"warnings\" : " + strWarnings.toString( ) + " } }";
return Response.status( Response.Status.CREATED ).entity( strResponse ).build( );
}
/**
* success case
*
* @return a successful response
*/
private Response success( )
{
return Response.status( Response.Status.CREATED ).entity( RESPONSE_OK ).build( );
}
/**
* Build an error response
*
* @param strMessage
* The error message
* @param ex
* An exception
* @return The response
*/
private Response fail( Throwable ex, Status httpStatus )
{
StringBuilder strMsg = new StringBuilder( "[" );
if ( ex != null )
{
AppLogService.error( ex.getMessage( ), ex );
strMsg.append( new StatusMessage( TYPE_NOTIFICATION, STATUS_ERROR, ex.toString( ) , ex.getMessage( ) ).asJson( ) );
}
strMsg.append( "]" );
String strError = "{ \"acknowledge\" : { \"status\": \"error\", \"errors\" : " + strMsg + " } }";
return Response.status( httpStatus ).entity( strError ).build( );
}
/**
* Generates the error message
* @param notification
* @param strResponseStatus
* @param strErrorMessage
* @return
*/
private String generateErrorMessage ( Notification notification, Status strResponseStatus, String strErrorMessage)
{
StringBuilder message = new StringBuilder();
message.append( "\n" );
message.append( "Demande id " + notification.getDemand( ).getId( ) + "\n" );
message.append( "Notification id " + notification.getId( ) + "\n" );
message.append( "Status: " + strResponseStatus.getStatusCode( ) + " " + strResponseStatus.getReasonPhrase( ) + "\n" );
message.append( "Error: " + strErrorMessage + "\n" );
return message.toString( );
}
/**
* Calculates the generic status id for new notifications.
* @param notification
* @return
*/
private int getNewDemandStatusIdFromNotification( Notification notification )
{
if ( notification.getMyDashboardNotification( ) != null )
{
// consider first if the status is sent by the demand
if ( notification.getDemand( ) != null
&& EnumGenericStatus.exists( notification.getDemand( ).getStatusId( ) ) )
{
return notification.getDemand( ).getStatusId( );
}
// consider the notification status id
if ( EnumGenericStatus.exists( notification.getMyDashboardNotification( ).getStatusId( ) ) )
{
return notification.getMyDashboardNotification( ).getStatusId( );
}
Optional<DemandStatus> status = _demandService.getStatusByLabel( notification.getMyDashboardNotification( ).getStatusText( ) );
if ( status.isPresent( ) && status.get( ).getGenericStatus( ) != null )
{
return status.get( ).getGenericStatus( ).getStatusId( );
}
return -1;
}
// default
return notification.getDemand( ).getStatusId( );
}
/**
* parse json
* @param strJson
* @return the notification
* @throws JsonMappingException
* @throws JsonProcessingException
*/
private Notification getNotificationFromJson( String strJson ) throws JsonMappingException, JsonProcessingException
{
AppLogService.debug( "notificationstore / notification - Received strJson : " + strJson );
// Format from JSON
ObjectMapper mapper = new ObjectMapper( );
mapper.configure( DeserializationFeature.UNWRAP_ROOT_VALUE, true );
mapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
Notification notification = mapper.readValue( strJson, Notification.class );
return notification;
}
/**
* call the registred notifyers
*
* @param notification
* @throws NotificationException
*/
public void forward( Notification notification ) throws NotificationException
{
for ( INotifyerServiceProvider notifyer : _notifyers )
{
notifyer.process( notification );
}
}
}