001/*
002 *  Copyright 2012 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.calendar.events;
017
018import java.time.LocalDate;
019import java.time.ZoneId;
020import java.time.ZonedDateTime;
021import java.time.format.DateTimeFormatter;
022import java.util.Date;
023
024import org.apache.avalon.framework.logger.LogEnabled;
025import org.apache.avalon.framework.logger.Logger;
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.avalon.framework.service.Serviceable;
029import org.apache.commons.lang.StringUtils;
030
031import org.ametys.cms.contenttype.ContentType;
032import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
033import org.ametys.cms.repository.Content;
034import org.ametys.core.util.DateUtils;
035import org.ametys.plugins.repository.AmetysObjectResolver;
036import org.ametys.runtime.model.ElementDefinition;
037import org.ametys.runtime.model.ModelItem;
038import org.ametys.runtime.model.type.ElementType;
039import org.ametys.runtime.model.type.ModelItemTypeConstants;
040
041/**
042 * Helper class that provides a method to check if a date is between two others.
043 */
044public final class EventHelper implements LogEnabled, Serviceable
045{    
046    /** The Ametys object resolver */
047    protected static AmetysObjectResolver _resolver;
048    /** The extension point for content types */
049    protected static ContentTypeExtensionPoint _contentTypeEP;
050    
051    private static Logger _logger;
052
053    @Override
054    public void enableLogging(Logger logger)
055    {
056        _logger = logger;
057    }
058    
059    @Override
060    public void service(ServiceManager manager) throws ServiceException
061    {
062        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
063        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
064    }
065    
066    /**
067     * Tests if the given date is either:
068     * - equal to the first, if there is no end date.
069     * - between the two dates, if there is a start and end date.
070     * @param dateStr the date to test.
071     * @param startStr start of the interval.
072     * @param endStr end of the interval.
073     * @return true if the date is either equal to the start date or between the start and end date.
074     */
075    public static boolean isBetween(String dateStr, String startStr, String endStr)
076    {
077        boolean between = false;
078        LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE);
079        LocalDate start = LocalDate.parse(startStr, DateTimeFormatter.ISO_LOCAL_DATE);
080        
081        if (StringUtils.isBlank(endStr))
082        {
083            between = date.equals(start);
084        }
085        else
086        {
087            LocalDate end = LocalDate.parse(endStr, DateTimeFormatter.ISO_LOCAL_DATE);
088            between = (date.isAfter(start) || date.equals(start)) && (date.isBefore(end) || date.equals(end));
089        }
090        
091        return between;
092    }
093    
094    /**
095     * Tests if the given event occurs in the given month.
096     * @param monthStartStr The month first day, cannot be null or blank.
097     * @param eventStartStr The event start date, can be null or blank only if the end date is set.
098     * @param eventEndStr The event end date, can be null or blank only if the end date is set.
099     * @return true if the event occurs in the given month.
100     */
101    public static boolean inMonth(String monthStartStr, String eventStartStr, String eventEndStr)
102    {
103        boolean overlaps = false;
104        
105        Date myDate = DateUtils.parse(monthStartStr);
106        ZonedDateTime monthStart = myDate.toInstant().atZone(ZoneId.systemDefault());
107        ZonedDateTime monthEnd = monthStart.toLocalDate().atStartOfDay(monthStart.getZone()).plusMonths(1);
108        
109        if (StringUtils.isNotBlank(eventStartStr) && StringUtils.isNotBlank(eventEndStr))
110        {
111            ZonedDateTime eventStart = DateUtils.parse(eventStartStr).toInstant().atZone(ZoneId.systemDefault());
112            ZonedDateTime eventEnd = DateUtils.parse(eventEndStr).toInstant().atZone(ZoneId.systemDefault());
113            
114            // check that start is before end to avoid an unwanted exception
115            try
116            {
117                // If eventStart equals to eventEnd and equals to monthStart, month#overlaps(Interval) will return false. In our case, we want to consider it actually does.
118                overlaps = eventStartStr.equals(eventEndStr) ? _contains(monthStart, monthEnd, eventStart)
119                                                             : _overlaps(monthStart, monthEnd, eventStart, eventEnd);
120            }
121            catch (IllegalArgumentException e)
122            {
123                // The end is before the start
124                overlaps = false;
125                _logger.error(String.format("Invalid dates of event: the end date (%s) must be greater or equal to the start date (%s). The event will be ignored in calendar view.", eventEndStr, eventStartStr), e);
126            }
127        }
128        else if (StringUtils.isNotBlank(eventStartStr))
129        {
130            ZonedDateTime eventStart = (DateUtils.parse(eventStartStr)).toInstant().atZone(ZoneId.systemDefault());
131            overlaps = _contains(monthStart, monthEnd, eventStart);
132        }
133        else if (StringUtils.isNotBlank(eventEndStr))
134        {
135            ZonedDateTime eventEnd = (DateUtils.parse(eventEndStr)).toInstant().atZone(ZoneId.systemDefault());
136            overlaps = _contains(monthStart, monthEnd, eventEnd);
137        }
138        
139        return overlaps;
140    }
141    
142    /**
143     * Test if an event is between 2 dates
144     * @param start start date
145     * @param end end date
146     * @param event event to check
147     * @return true if the event is between both dates
148     */
149    private static boolean _contains(ZonedDateTime start, ZonedDateTime end, ZonedDateTime event)
150    {
151        ZonedDateTime realStart;
152        ZonedDateTime realEnd;
153        if (start.isBefore(end))
154        {
155            realStart = start;
156            realEnd = end;
157        }
158        else
159        {
160            realStart = end;
161            realEnd = start;
162        }
163        return (realStart.isBefore(event) || realStart.isEqual(event)) && realEnd.isAfter(event);
164    }
165    
166    /**
167     * Test if 2 period of time overlap
168     * @param start1 start of 1st period
169     * @param end1 end of 1st period
170     * @param start2 start of 2nd period
171     * @param end2 end of 2nd period
172     * @return true if both period overlaps
173     */
174    private static boolean _overlaps(ZonedDateTime start1, ZonedDateTime end1, ZonedDateTime start2, ZonedDateTime end2)
175    {
176        ZonedDateTime realStart1;
177        ZonedDateTime realEnd1;
178        if (start1.isBefore(end1))
179        {
180            realStart1 = start1;
181            realEnd1 = end1;
182        }
183        else
184        {
185            realStart1 = end1;
186            realEnd1 = start1;
187        }
188        ZonedDateTime realStart2;
189        ZonedDateTime realEnd2;
190        if (start2.isBefore(end2))
191        {
192            realStart2 = start2;
193            realEnd2 = end2;
194        }
195        else
196        {
197            realStart2 = end2;
198            realEnd2 = start2;
199        }
200        return (realStart1.isBefore(realEnd2) || realStart1.isEqual(realEnd2)) && realEnd1.isAfter(realStart2);
201    }
202    
203    /**
204     * Get the next day
205     * @param dateStr the zoned date time
206     * @return the next day as iso zoned date time
207     */
208    public static String nextDay(String dateStr)
209    {
210        ZonedDateTime zonedDateTime = DateUtils.parseZonedDateTime(dateStr);
211        zonedDateTime = zonedDateTime.plusDays(1);
212        
213        return DateUtils.zonedDateTimeToString(zonedDateTime);
214    }
215    
216    /**
217     * Get the next day
218     * @param dateStr the zoned date time
219     * @param format the output format: "date" for ISO local date or "basicDate" to basic ISO date.
220     * @return the next day at requested output format
221     */
222    public static String nextDay(String dateStr, String format)
223    {
224        String nextDayStr = "";
225        
226        ZonedDateTime date = ZonedDateTime.parse(dateStr, DateUtils.getISODateTimeFormatter());
227        
228        ZonedDateTime nextDay = date.plusDays(1);
229        
230        if ("basicDate".equals(format))
231        {
232            nextDayStr = DateTimeFormatter.BASIC_ISO_DATE.format(nextDay);
233        }
234        else
235        {
236            nextDayStr = DateTimeFormatter.ISO_LOCAL_DATE.format(nextDay);
237        }
238        
239        return nextDayStr;
240    }
241    
242    /**
243     * Tests if the given attribute for the given content is of type datetime
244     * @param contentId The id of the content
245     * @param attributeName The attribute name
246     * @return true if the given attribute for the given content is of type datetime
247     */
248    public static boolean isDatetime(String contentId, String attributeName)
249    {
250        Content content = _resolver.resolveById(contentId);
251        String[] contentTypeIds = content.getTypes();
252        
253        for (String contentTypeId : contentTypeIds)
254        {
255            ContentType contentType = _contentTypeEP.getExtension(contentTypeId);
256            
257            if (contentType.hasModelItem(attributeName))
258            {
259                ModelItem definition = contentType.getModelItem(attributeName);
260                if (definition instanceof ElementDefinition)
261                {
262                    ElementType type = ((ElementDefinition) definition).getType();
263                    return ModelItemTypeConstants.DATETIME_TYPE_ID.equals(type.getId());
264                }
265                else
266                {
267                    // The given attribute is a group item
268                    return false;
269                }
270            }
271        }
272        
273        return false;
274    }
275    
276}