001/*
002 *  Copyright 2022 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.hyperplanning;
017
018import java.time.Duration;
019import java.time.ZonedDateTime;
020import java.time.temporal.ChronoUnit;
021import java.util.Comparator;
022import java.util.GregorianCalendar;
023import java.util.List;
024import java.util.Set;
025import java.util.TreeSet;
026
027import javax.xml.datatype.DatatypeConfigurationException;
028import javax.xml.datatype.DatatypeFactory;
029import javax.xml.datatype.XMLGregorianCalendar;
030import javax.xml.ws.BindingProvider;
031import javax.xml.ws.WebServiceException;
032
033import org.apache.avalon.framework.activity.Initializable;
034import org.apache.avalon.framework.component.Component;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038
039import org.ametys.core.cache.AbstractCacheManager;
040import org.ametys.core.cache.Cache;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.runtime.config.Config;
043import org.ametys.runtime.i18n.I18nizableText;
044import org.ametys.runtime.plugin.component.AbstractLogEnabled;
045
046import com.indexeducation.frahtm.hpsvcw.HpSvcWDonnees;
047import com.indexeducation.frahtm.hpsvcw.IHpSvcWCoursAnnules;
048import com.indexeducation.frahtm.hpsvcw.IHpSvcWEtudiants;
049import com.indexeducation.frahtm.hpsvcw.IHpSvcWMatieres;
050import com.indexeducation.frahtm.hpsvcw.THpSvcWCleCoursAnnule;
051import com.indexeducation.frahtm.hpsvcw.THpSvcWCleEtudiant;
052import com.indexeducation.frahtm.hpsvcw.THpSvcWTableauClesCoursAnnules;
053import com.indexeducation.frahtm.hpsvcw.THpSvcWTableauClesMatieres;
054import com.indexeducation.frahtm.hpsvcw.THpSvcWTableauTypesSeances;
055import com.indexeducation.frahtm.hpsvcw.THpSvcWTypeSeance;
056
057/**
058 * Component handling the communication with a remote hyperplanning server
059 */
060public class HyperplanningManager extends AbstractLogEnabled implements Initializable, Component, Serviceable
061{
062    /** The avalon role */
063    public static final String ROLE = HyperplanningManager.class.getName();
064    private static final String __CANCELLED_LESSONS_CACHE = HyperplanningManager.class.getName() + "$cancelledLessons";
065    
066    private String _connectionLogin;
067    private String _connectionPass;
068    private String _serverUrl;
069    
070    private AbstractCacheManager _cacheManager;
071    
072    private HpSvcWDonnees _dataService;
073    private IHpSvcWCoursAnnules _cancelledCoursesPort;
074    private IHpSvcWEtudiants _studentPort;
075    private IHpSvcWMatieres _subjectPort;
076    private HyperplannningStudentProvider _studentProvider;
077    
078    public void service(ServiceManager manager) throws ServiceException
079    {
080        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
081        _studentProvider = (HyperplannningStudentProvider) manager.lookup(HyperplannningStudentProvider.ROLE);
082    }
083    
084    public void initialize() throws Exception
085    {
086        _connectionLogin = Config.getInstance().getValue("org.ametys.plugins.hyperplanning.login");
087        _connectionPass = Config.getInstance().getValue("org.ametys.plugins.hyperplanning.password");
088        _serverUrl = Config.getInstance().getValue("org.ametys.plugins.hyperplanning.url");
089        
090        _cacheManager.createMemoryCache(__CANCELLED_LESSONS_CACHE,
091                new I18nizableText("plugin.hyperplanning", "PLUGIN_HYPERPLANNING_CANCELLED_LESSONS_CACHE_LABEL"),
092                new I18nizableText("plugin.hyperplanning", "PLUGIN_HYPERPLANNING_CANCELLED_LESSONS_CACHE_DESCRIPTION"),
093                true,
094                Duration.ofMinutes(Config.getInstance().getValue("org.ametys.plugins.hyperplanning.cache-validity")));
095        
096        _initializeWebService();
097    }
098    
099    /**
100     * Get the list of cancelled lessons for a given user in the next 2 weeks
101     * @param userIdentity the user identity
102     * @return a list of {@link CancelledLesson} representing the cancelled lessons or null if the user is unknown
103     * @throws UnknownStudentException when user is not linked to hyperplanning
104     */
105    public Set<CancelledLesson> getUpcomingCancelledLessons(UserIdentity userIdentity) throws UnknownStudentException
106    {
107        if (userIdentity == null)
108        {
109            throw new IllegalArgumentException("User is not connected");
110        }
111        
112        Set<CancelledLesson> cancelledLessons = getCache().get(userIdentity, this::_getCancelledLessons);
113        if (cancelledLessons == null)
114        {
115            throw new UnknownStudentException("User '" + userIdentity + "' has no link hyperplanning id");
116        }
117        
118        return cancelledLessons;
119    }
120    
121    /**
122     * Get the list of cancelled lessons for a given user in the next 2 weeks
123     * @param userIdentity the user identity
124     * @return a list of {@link CancelledLesson} representing the cancelled lessons or null if the user is not linked to hyperplanning
125     */
126    private Set<CancelledLesson> _getCancelledLessons(UserIdentity userIdentity)
127    {
128        String hypIdentity = _studentProvider.getStudentId(userIdentity);
129        THpSvcWCleEtudiant identifiant = null;
130        try
131        {
132            identifiant = _studentPort.accederEtudiantParIdentifiant(hypIdentity);
133        }
134        catch (WebServiceException e)
135        {
136            // null means that the user is not linked
137            getLogger().warn("User '" + userIdentity + "' has no link hyperplanning id");
138            return null;
139        }
140        
141        ZonedDateTime startDate = ZonedDateTime.now().truncatedTo(ChronoUnit.DAYS);
142        ZonedDateTime endDate = startDate.plusWeeks(2).plusDays(1); // add one day to 'compensate' the rounding
143        XMLGregorianCalendar startXmlCalendar = null;
144        XMLGregorianCalendar endXmlCalendar = null;
145        try
146        {
147            startXmlCalendar = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar.from(startDate));
148            endXmlCalendar   = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar.from(endDate));
149        }
150        catch (DatatypeConfigurationException e)
151        {
152            throw new RuntimeException("Can't load xmlGregorian datatype implementation", e);
153        }
154        
155        THpSvcWTableauClesCoursAnnules cancelledCourses = _cancelledCoursesPort.coursAnnulesParEtudiantEntre2Dates(identifiant, startXmlCalendar, endXmlCalendar);
156
157        // retrieve the info related to the courses
158        List<THpSvcWCleCoursAnnule> cancelledCourseKeys = cancelledCourses.getTHpSvcWCleCoursAnnule();
159        List<String> cancelledCoursesRationale = _cancelledCoursesPort.motifAnnulationTableauDeCoursAnnules(cancelledCourses).getString();
160        List<String> cancelledCoursesComment = _cancelledCoursesPort.commentairesTableauDeCoursAnnules(cancelledCourses).getString();
161        
162        // retrieve info related to the subject of the courses
163        THpSvcWTableauClesMatieres cancelledCoursesSubject = _cancelledCoursesPort.matieresTableauDeCoursAnnules(cancelledCourses);
164        List<String> cancelledCoursesSubjectCode = _subjectPort.codesTableauDeMatieres(cancelledCoursesSubject).getString();
165        List<String> cancelledCourseSubjectLabel = _subjectPort.libellesTableauDeMatieres(cancelledCoursesSubject).getString();
166        List<String> cancelledCoursesSubjectLongLabel = _subjectPort.libellesLongsTableauDeMatieres(cancelledCoursesSubject).getString();
167        
168        Comparator<CancelledLesson> comparator = (c1, c2) -> c1.getDate().compareTo(c2.getDate());
169        Set<CancelledLesson> result = new TreeSet<>(comparator);
170        
171        // For each cancelled courses, iterate on the lesson to retrieve the information and store the lesson in a single set ordered by lesson date
172        int size = cancelledCourseKeys.size();
173        for (int i = 0; i < size; i++)
174        {
175            THpSvcWCleCoursAnnule cancelledCourseKey = cancelledCourseKeys.get(i);
176            // Retrieve all the lessons cancelled by this cancelledCourseKey. Some lessons can be outside of the time windows and need to be filtered
177            THpSvcWTableauTypesSeances cancelledLessons = _cancelledCoursesPort.detailDesSeancesPlaceesDuCoursAnnule(cancelledCourseKey);
178        
179            for (THpSvcWTypeSeance lessons : cancelledLessons.getTHpSvcWTypeSeance())
180            {
181                ZonedDateTime date = lessons.getJourEtHeureDebut().toGregorianCalendar().toZonedDateTime();
182                if (date.isAfter(startDate) && date.isBefore(endDate))
183                {
184                    result.add(new CancelledLesson(cancelledCourseKey,
185                            cancelledCoursesSubjectCode.get(i),
186                            cancelledCourseSubjectLabel.get(i),
187                            cancelledCoursesSubjectLongLabel.get(i),
188                            date,
189                            cancelledCoursesRationale.get(i),
190                            cancelledCoursesComment.get(i)
191                            ));
192                }
193            }
194        }
195        
196        return result;
197    }
198
199    // Long operation
200    private void _initializeWebService()
201    {
202        _dataService = new HpSvcWDonnees();
203        
204        _cancelledCoursesPort = _dataService.getPortCoursAnnules();
205        ((BindingProvider) _cancelledCoursesPort).getRequestContext().put(BindingProvider.USERNAME_PROPERTY, _connectionLogin);
206        ((BindingProvider) _cancelledCoursesPort).getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, _connectionPass);
207        ((BindingProvider) _cancelledCoursesPort).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, _serverUrl);
208        
209        _studentPort = _dataService.getPortEtudiants();
210        ((BindingProvider) _studentPort).getRequestContext().put(BindingProvider.USERNAME_PROPERTY, _connectionLogin);
211        ((BindingProvider) _studentPort).getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, _connectionPass);
212        ((BindingProvider) _studentPort).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, _serverUrl);
213        
214        _subjectPort = _dataService.getPortMatieres();
215        ((BindingProvider) _subjectPort).getRequestContext().put(BindingProvider.USERNAME_PROPERTY, _connectionLogin);
216        ((BindingProvider) _subjectPort).getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, _connectionPass);
217        ((BindingProvider) _subjectPort).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, _serverUrl);
218    }
219    
220    private Cache<UserIdentity, Set<CancelledLesson>> getCache()
221    {
222        return _cacheManager.get(__CANCELLED_LESSONS_CACHE);
223    }
224}