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.contenttype.MetadataDefinition;
034import org.ametys.cms.contenttype.MetadataType;
035import org.ametys.cms.repository.Content;
036import org.ametys.plugins.repository.AmetysObjectResolver;
037import org.ametys.runtime.parameter.ParameterHelper;
038import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
039
040/**
041 * Helper class that provides a method to check if a date is between two others.
042 */
043public final class EventHelper implements LogEnabled, Serviceable
044{    
045    /** The Ametys object resolver */
046    protected static AmetysObjectResolver _resolver;
047    /** The extension point for content types */
048    protected static ContentTypeExtensionPoint _contentTypeEP;
049    
050    private static Logger _logger;
051
052    @Override
053    public void enableLogging(Logger logger)
054    {
055        _logger = logger;
056    }
057    
058    @Override
059    public void service(ServiceManager manager) throws ServiceException
060    {
061        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
062        _contentTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
063    }
064    
065    /**
066     * Tests if the given date is either:
067     * - equal to the first, if there is no end date.
068     * - between the two dates, if there is a start and end date.
069     * @param dateStr the date to test.
070     * @param startStr start of the interval.
071     * @param endStr end of the interval.
072     * @return true if the date is either equal to the start date or between the start and end date.
073     */
074    public static boolean isBetween(String dateStr, String startStr, String endStr)
075    {
076        boolean between = false;
077        LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE);
078        LocalDate start = LocalDate.parse(startStr, DateTimeFormatter.ISO_LOCAL_DATE);
079        
080        if (StringUtils.isBlank(endStr))
081        {
082            between = date.equals(start);
083        }
084        else
085        {
086            LocalDate end = LocalDate.parse(endStr, DateTimeFormatter.ISO_LOCAL_DATE);
087            between = (date.isAfter(start) || date.equals(start)) && (date.isBefore(end) || date.equals(end));
088        }
089        
090        return between;
091    }
092    
093    /**
094     * Tests if the given event occurs in the given month.
095     * @param monthStartStr The month first day, cannot be null or blank.
096     * @param eventStartStr The event start date, can be null or blank only if the end date is set.
097     * @param eventEndStr The event end date, can be null or blank only if the end date is set.
098     * @return true if the event occurs in the given month.
099     */
100    public static boolean inMonth(String monthStartStr, String eventStartStr, String eventEndStr)
101    {
102        boolean overlaps = false;
103        
104        Date myDate = (Date) ParameterHelper.castValue(monthStartStr, ParameterType.DATE);
105        ZonedDateTime monthStart = myDate.toInstant().atZone(ZoneId.systemDefault());
106        ZonedDateTime monthEnd = monthStart.toLocalDate().atStartOfDay(monthStart.getZone()).plusMonths(1);
107        
108        if (StringUtils.isNotBlank(eventStartStr) && StringUtils.isNotBlank(eventEndStr))
109        {
110            ZonedDateTime eventStart = ((Date) ParameterHelper.castValue(eventStartStr, ParameterType.DATE)).toInstant().atZone(ZoneId.systemDefault());
111            ZonedDateTime eventEnd = ((Date) ParameterHelper.castValue(eventEndStr, ParameterType.DATE)).toInstant().atZone(ZoneId.systemDefault());
112            
113            // check that start is before end to avoid an unwanted exception
114            try
115            {
116                // 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.
117                overlaps = eventStartStr.equals(eventEndStr) ? _contains(monthStart, monthEnd, eventStart)
118                                                             : _overlaps(monthStart, monthEnd, eventStart, eventEnd);
119            }
120            catch (IllegalArgumentException e)
121            {
122                // The end is before the start
123                overlaps = false;
124                _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);
125            }
126        }
127        else if (StringUtils.isNotBlank(eventStartStr))
128        {
129            ZonedDateTime eventStart = ((Date) ParameterHelper.castValue(eventStartStr, ParameterType.DATE)).toInstant().atZone(ZoneId.systemDefault());
130            overlaps = _contains(monthStart, monthEnd, eventStart);
131        }
132        else if (StringUtils.isNotBlank(eventEndStr))
133        {
134            ZonedDateTime eventEnd = ((Date) ParameterHelper.castValue(eventEndStr, ParameterType.DATE)).toInstant().atZone(ZoneId.systemDefault());
135            overlaps = _contains(monthStart, monthEnd, eventEnd);
136        }
137        
138        return overlaps;
139    }
140    
141    /**
142     * Test if an event is between 2 dates
143     * @param start start date
144     * @param end end date
145     * @param event event to check
146     * @return true if the event is between both dates
147     */
148    private static boolean _contains(ZonedDateTime start, ZonedDateTime end, ZonedDateTime event)
149    {
150        ZonedDateTime realStart;
151        ZonedDateTime realEnd;
152        if (start.isBefore(end))
153        {
154            realStart = start;
155            realEnd = end;
156        }
157        else
158        {
159            realStart = end;
160            realEnd = start;
161        }
162        return (realStart.isBefore(event) || realStart.isEqual(event)) && realEnd.isAfter(event);
163    }
164    
165    /**
166     * Test if 2 period of time overlap
167     * @param start1 start of 1st period
168     * @param end1 end of 1st period
169     * @param start2 start of 2nd period
170     * @param end2 end of 2nd period
171     * @return true if both period overlaps
172     */
173    private static boolean _overlaps(ZonedDateTime start1, ZonedDateTime end1, ZonedDateTime start2, ZonedDateTime end2)
174    {
175        ZonedDateTime realStart1;
176        ZonedDateTime realEnd1;
177        if (start1.isBefore(end1))
178        {
179            realStart1 = start1;
180            realEnd1 = end1;
181        }
182        else
183        {
184            realStart1 = end1;
185            realEnd1 = start1;
186        }
187        ZonedDateTime realStart2;
188        ZonedDateTime realEnd2;
189        if (start2.isBefore(end2))
190        {
191            realStart2 = start2;
192            realEnd2 = end2;
193        }
194        else
195        {
196            realStart2 = end2;
197            realEnd2 = start2;
198        }
199        return (realStart1.isBefore(realEnd2) || realStart1.isEqual(realEnd2)) && realEnd1.isAfter(realStart2);
200    }
201    
202    /**
203     * Tests if the given date is either:
204     * @param dateStr the date to test.
205     * @return true if the date is either equal to the start date or between the start and end date.
206     */
207    public static String nextDay(String dateStr)
208    {
209        ZonedDateTime date = ZonedDateTime.parse(dateStr, DateTimeFormatter.ISO_DATE_TIME);
210
211        return ParameterHelper.getISODateTimeFormatter().format(date.plusDays(1));
212    }
213    
214    /**
215     * Tests if the given date is either:
216     * @param dateStr the date to test.
217     * @param format the format ("date" or "basicDate").
218     * @return true if the date is either equal to the start date or between the start and end date.
219     */
220    public static String nextDay(String dateStr, String format)
221    {
222        String nextDayStr = "";
223        
224        ZonedDateTime date = ZonedDateTime.parse(dateStr, ParameterHelper.getISODateTimeFormatter());
225        
226        ZonedDateTime nextDay = date.plusDays(1);
227        
228        if ("basicDate".equals(format))
229        {
230            nextDayStr = DateTimeFormatter.BASIC_ISO_DATE.format(nextDay);
231        }
232        else
233        {
234            nextDayStr = DateTimeFormatter.ISO_LOCAL_DATE.format(nextDay);
235        }
236        
237        return nextDayStr;
238    }
239    
240    /**
241     * Tests if the given metadata for the given content is of type {@link MetadataType#DATETIME}
242     * @param contentId The id of the content
243     * @param metadataName The metadata name
244     * @return true if the given metadata for the given content is of type {@link MetadataType#DATETIME}
245     */
246    public static boolean isDatetime(String contentId, String metadataName)
247    {
248        Content content = _resolver.resolveById(contentId);
249        String[] cTypes = content.getTypes();
250        
251        for (String cTypeId : cTypes)
252        {
253            ContentType cType = _contentTypeEP.getExtension(cTypeId);
254            
255            if (cType.hasMetadataDefinition(metadataName))
256            {
257                MetadataDefinition metadataDef = cType.getMetadataDefinition(metadataName);
258                MetadataType type = metadataDef.getType();
259                
260                return MetadataType.DATETIME.equals(type);
261            }
262        }
263        
264        return false;
265    }
266}