1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package fr.paris.lutece.plugins.workflow.modules.appointment.service;
35
36 import java.net.URI;
37 import java.net.URISyntaxException;
38 import java.time.ZoneId;
39 import java.util.StringTokenizer;
40
41 import org.apache.commons.lang3.StringUtils;
42
43 import fr.paris.lutece.plugins.appointment.business.appointment.Appointment;
44 import fr.paris.lutece.plugins.appointment.web.dto.AppointmentDTO;
45 import fr.paris.lutece.portal.service.mail.MailService;
46 import fr.paris.lutece.portal.service.spring.SpringContextService;
47 import fr.paris.lutece.portal.service.util.AppLogService;
48 import fr.paris.lutece.portal.service.util.AppPathService;
49 import fr.paris.lutece.portal.service.util.AppPropertiesService;
50 import java.io.FileInputStream;
51 import java.io.FileNotFoundException;
52 import java.io.IOException;
53 import net.fortuna.ical4j.data.CalendarBuilder;
54 import net.fortuna.ical4j.data.ParserException;
55 import net.fortuna.ical4j.model.Calendar;
56 import net.fortuna.ical4j.model.DateTime;
57 import net.fortuna.ical4j.model.ParameterList;
58 import net.fortuna.ical4j.model.TimeZone;
59 import net.fortuna.ical4j.model.TimeZoneRegistry;
60 import net.fortuna.ical4j.model.component.VEvent;
61 import net.fortuna.ical4j.model.parameter.Cn;
62 import net.fortuna.ical4j.model.parameter.PartStat;
63 import net.fortuna.ical4j.model.parameter.Role;
64 import net.fortuna.ical4j.model.parameter.Rsvp;
65 import net.fortuna.ical4j.model.parameter.XParameter;
66 import net.fortuna.ical4j.model.property.Attendee;
67 import net.fortuna.ical4j.model.property.CalScale;
68 import net.fortuna.ical4j.model.property.Description;
69 import net.fortuna.ical4j.model.property.DtEnd;
70 import net.fortuna.ical4j.model.property.DtStart;
71 import net.fortuna.ical4j.model.property.Location;
72 import net.fortuna.ical4j.model.property.Method;
73 import net.fortuna.ical4j.model.property.Organizer;
74 import net.fortuna.ical4j.model.property.ProdId;
75 import net.fortuna.ical4j.model.property.Summary;
76 import net.fortuna.ical4j.model.property.Uid;
77 import net.fortuna.ical4j.model.property.Version;
78 import net.fortuna.ical4j.model.property.XProperty;
79
80
81
82
83 public class ICalService
84 {
85
86
87
88 public static final String BEAN_NAME = "workflow-appointment.iCalService";
89
90
91 private static final String PROPERTY_MAIL_LIST_SEPARATOR = "mail.list.separator";
92 private static final String PROPERTY_ICAL_PRODID = "workflow-appointment.ical.prodid";
93 private static final String PROPERTY_DEFAULT_TIME_ZONE = "workflow-appointment.server.timezone.id";
94 private static final String PROPERTY_RELATIVE_PATH_TO_TIME_ZONE_FILE = "workflow-appointment.server.timezone.fileRelativePath";
95
96
97 private static final String CONSTANT_MAILTO = "MAILTO:";
98
99
100 private static final String MSG_TIMEZONE_FILE_NOT_FOUND = "iCal default Time zone file not found";
101 private static final String MSG_TIMEZONE_FILE_INCORRECT = "iCal default Time zone file format problem";
102
103
104
105
106
107
108 public static ICalService getService( )
109 {
110 return SpringContextService.getBean( BEAN_NAME );
111 }
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135 public void sendAppointment( String strEmailAttendee, String strEmailOptionnal, String strSubject, String strBodyContent, String strLocation,
136 String strSenderName, String strSenderEmail, AppointmentDTO appointment, boolean bCreate )
137 {
138
139 CalendarBuilder builder = new CalendarBuilder( );
140 Calendar iCalendar;
141 try
142 {
143 String strRelativeWebPath = AppPropertiesService.getProperty( PROPERTY_RELATIVE_PATH_TO_TIME_ZONE_FILE );
144 String absoluteTimeZoneFilePath = AppPathService.getAbsolutePathFromRelativePath( strRelativeWebPath );
145 iCalendar = builder.build( new FileInputStream( absoluteTimeZoneFilePath ) );
146 }
147 catch( FileNotFoundException ex )
148 {
149 AppLogService.error( MSG_TIMEZONE_FILE_NOT_FOUND, ex );
150 return;
151 }
152 catch( IOException | ParserException ex )
153 {
154 AppLogService.error( MSG_TIMEZONE_FILE_INCORRECT, ex );
155 return;
156 }
157
158 TimeZoneRegistry registry = builder.getRegistry( );
159 TimeZone timeZone = registry.getTimeZone( AppPropertiesService.getProperty( PROPERTY_DEFAULT_TIME_ZONE ) );
160
161 DateTime beginningDateTime = new DateTime( appointment.getStartingDateTime( ).atZone( ZoneId.systemDefault( ) ).toInstant( ).toEpochMilli( ) );
162 DateTime endingDateTime = new DateTime( appointment.getEndingDateTime( ).atZone( ZoneId.systemDefault( ) ).toInstant( ).toEpochMilli( ) );
163
164 DtStart dtStart = new DtStart( beginningDateTime );
165 dtStart.setTimeZone( timeZone );
166
167 DtEnd dtEnd = new DtEnd( endingDateTime );
168 dtEnd.setTimeZone( timeZone );
169
170 VEvent event = new VEvent( );
171 event.getProperties( ).add( dtStart );
172 event.getProperties( ).add( dtEnd );
173 event.getProperties( ).add( new Summary( ( strSubject != null ) ? strSubject : StringUtils.EMPTY ) );
174
175
176 String formatedIcalendarDescription = formatICalendarDescription( strBodyContent );
177
178 try
179 {
180 event.getProperties( ).add( new Uid( Appointment.APPOINTMENT_RESOURCE_TYPE + appointment.getIdAppointment( ) ) );
181 String strEmailSeparator = AppPropertiesService.getProperty( PROPERTY_MAIL_LIST_SEPARATOR, ";" );
182 if ( StringUtils.isNotEmpty( strEmailAttendee ) )
183 {
184 StringTokenizer st = new StringTokenizer( strEmailAttendee, strEmailSeparator );
185 while ( st.hasMoreTokens( ) )
186 {
187 addAttendee( event, st.nextToken( ), true );
188 }
189 }
190 if ( StringUtils.isNotEmpty( strEmailOptionnal ) )
191 {
192 StringTokenizer st = new StringTokenizer( strEmailOptionnal, strEmailSeparator );
193 while ( st.hasMoreTokens( ) )
194 {
195 addAttendee( event, st.nextToken( ), false );
196 }
197 }
198 Organizer organizer = new Organizer( strSenderEmail );
199 organizer.getParameters( ).add( new Cn( strSenderName ) );
200 event.getProperties( ).add( organizer );
201 event.getProperties( ).add( new Location( strLocation ) );
202 event.getProperties( ).add( new Description( formatedIcalendarDescription ) );
203
204 addAlternativeHtmlDescription( event, formatedIcalendarDescription );
205 }
206 catch( URISyntaxException e )
207 {
208 AppLogService.error( e.getMessage( ), e );
209 }
210
211 iCalendar.getProperties( ).add( bCreate ? Method.REQUEST : Method.CANCEL );
212 iCalendar.getProperties( ).add( new ProdId( AppPropertiesService.getProperty( PROPERTY_ICAL_PRODID ) ) );
213 iCalendar.getProperties( ).add( Version.VERSION_2_0 );
214 iCalendar.getProperties( ).add( CalScale.GREGORIAN );
215 iCalendar.getComponents( ).add( event );
216
217 MailService.sendMailCalendar( strEmailAttendee, strEmailOptionnal, null, strSenderName, strSenderEmail,
218 ( strSubject != null ) ? strSubject : StringUtils.EMPTY, strBodyContent, iCalendar.toString( ), bCreate );
219 }
220
221
222
223
224
225
226
227
228
229
230
231 private void addAttendee( VEvent event, String strEmail, boolean bRequired )
232 {
233 Attendee attendee = new Attendee( URI.create( CONSTANT_MAILTO + strEmail ) );
234 attendee.getParameters( ).add( bRequired ? Role.REQ_PARTICIPANT : Role.OPT_PARTICIPANT );
235 attendee.getParameters( ).add( PartStat.NEEDS_ACTION );
236 attendee.getParameters( ).add( Rsvp.FALSE );
237 event.getProperties( ).add( attendee );
238 }
239
240
241
242
243
244
245
246
247
248 public static String formatICalendarDescription( String strDescription )
249 {
250
251 String foldingSymbol = "\r ";
252 int lineMaxLength = 75;
253
254 int descriptionLength = strDescription.length( );
255
256 if ( descriptionLength <= lineMaxLength )
257 {
258 return strDescription;
259 }
260
261 strDescription = strDescription.replaceAll( "\n", foldingSymbol );
262
263 StringBuilder formatedDesctiption = new StringBuilder( strDescription.substring( 0, lineMaxLength ) );
264
265
266 for ( int i = lineMaxLength; i < descriptionLength; i += lineMaxLength )
267 {
268 if ( i + lineMaxLength < descriptionLength )
269 {
270
271 formatedDesctiption.append( foldingSymbol ).append( strDescription.substring( i, i + lineMaxLength ) );
272 }
273 else
274 {
275
276 formatedDesctiption.append( foldingSymbol ).append( strDescription.substring( i ) );
277 break;
278 }
279 }
280 return formatedDesctiption.toString( );
281 }
282
283
284
285
286
287
288
289
290
291 public void addAlternativeHtmlDescription( VEvent event, String description )
292 {
293
294 String patternHtml = "[\\S\\s]*\\<\\D+[\\S\\s]*\\>[\\S\\s]*\\<\\/\\D+[\\S\\s]*\\>[\\S\\s]*";
295
296
297 if ( description.matches( patternHtml ) )
298 {
299
300 ParameterList htmlParameters = new ParameterList( );
301 XParameter fmtTypeParameter = new XParameter( "FMTTYPE", "text/html" );
302 htmlParameters.add( fmtTypeParameter );
303 XProperty htmlProp = new XProperty( "X-ALT-DESC", htmlParameters, description );
304
305
306 event.getProperties( ).add( htmlProp );
307 }
308 }
309 }