View Javadoc

1   /**
2    * Copyright 2012 The Kuali Foundation Licensed under the
3    * Educational Community License, Version 2.0 (the "License"); you may
4    * not use this file except in compliance with the License. You may
5    * obtain a copy of the License at
6    *
7    * http://www.osedu.org/licenses/ECL-2.0
8    *
9    * Unless required by applicable law or agreed to in writing,
10   * software distributed under the License is distributed on an "AS IS"
11   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing
13   * permissions and limitations under the License.
14   *
15   * Created by vgadiyak on 9/10/12
16   */
17  package org.kuali.student.enrollment.class2.scheduleofclasses.service.impl;
18  
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.criteria.PredicateFactory;
21  import org.kuali.rice.core.api.criteria.QueryByCriteria;
22  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
23  import org.kuali.rice.kim.api.identity.Person;
24  import org.kuali.rice.kim.api.identity.PersonService;
25  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
26  import org.kuali.rice.kim.impl.KIMPropertyConstants;
27  import org.kuali.rice.krad.uif.service.impl.ViewHelperServiceImpl;
28  import org.kuali.rice.krad.util.GlobalVariables;
29  import org.kuali.student.enrollment.class2.scheduleofclasses.dto.ActivityOfferingDisplayWrapper;
30  import org.kuali.student.enrollment.class2.scheduleofclasses.dto.CourseOfferingDisplayWrapper;
31  import org.kuali.student.enrollment.class2.scheduleofclasses.form.ScheduleOfClassesSearchForm;
32  import org.kuali.student.enrollment.class2.scheduleofclasses.service.ScheduleOfClassesViewHelperService;
33  import org.kuali.student.enrollment.class2.scheduleofclasses.util.ScheduleOfClassesConstants;
34  import org.kuali.student.enrollment.courseoffering.dto.ActivityOfferingDisplayInfo;
35  import org.kuali.student.enrollment.courseoffering.dto.CourseOfferingDisplayInfo;
36  import org.kuali.student.enrollment.courseoffering.service.CourseOfferingService;
37  import org.kuali.student.enrollment.lpr.service.LprService;
38  import org.kuali.student.r2.common.constants.CommonServiceConstants;
39  import org.kuali.student.r2.common.dto.ContextInfo;
40  import org.kuali.student.r2.common.dto.KeyNameInfo;
41  import org.kuali.student.r2.common.util.ContextUtils;
42  import org.kuali.student.r2.common.util.constants.CourseOfferingServiceConstants;
43  import org.kuali.student.r2.common.util.constants.LprServiceConstants;
44  import org.kuali.student.r2.common.util.constants.LuiServiceConstants;
45  import org.kuali.student.r2.common.util.date.DateFormatters;
46  import org.kuali.student.r2.core.organization.service.OrganizationService;
47  import org.kuali.student.r2.core.scheduling.constants.SchedulingServiceConstants;
48  import org.kuali.student.r2.core.scheduling.infc.ScheduleComponentDisplay;
49  import org.kuali.student.r2.lum.util.constants.LrcServiceConstants;
50  
51  import javax.xml.namespace.QName;
52  import java.util.ArrayList;
53  import java.util.Calendar;
54  import java.util.HashMap;
55  import java.util.List;
56  import java.util.Map;
57  
58  /**
59   * This class performs queries for scheduling of classes
60   *
61   * @author Kuali Student Team
62   */
63  public class ScheduleOfClassesViewHelperServiceImpl extends ViewHelperServiceImpl implements ScheduleOfClassesViewHelperService {
64  
65      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ScheduleOfClassesViewHelperServiceImpl.class);
66  
67      private CourseOfferingService coService;
68      private LprService lprService;
69      private OrganizationService organizationService;
70  
71      public void loadCourseOfferingsByTermAndCourseCode(String termId, String courseCode, ScheduleOfClassesSearchForm form) throws Exception{
72  
73          ContextInfo contextInfo = ContextUtils.createDefaultContextInfo();
74  
75          // Building a query
76          QueryByCriteria.Builder qbcBuilder = QueryByCriteria.Builder.create();
77          qbcBuilder.setPredicates(PredicateFactory.and(
78                  PredicateFactory.like("courseOfferingCode", courseCode + "%"),
79                  PredicateFactory.equalIgnoreCase("atpId", termId)),
80                  PredicateFactory.equal("luiState", LuiServiceConstants.LUI_CO_STATE_OFFERED_KEY));
81          QueryByCriteria criteria = qbcBuilder.build();
82          List<String> courseOfferingIds = getCourseOfferingService().searchForCourseOfferingIds(criteria, contextInfo);
83  
84          if(courseOfferingIds.size() > 0){
85              form.getCoDisplayWrapperList().clear();
86              form.setCoDisplayWrapperList(getCourseOfferingDisplayWrappersByIds(courseOfferingIds,getCourseOfferingService(),contextInfo));
87          } else {
88              LOG.error("Error: Can't find any Course Offering for a Course Code: " + courseCode + " in term: " + termId);
89              GlobalVariables.getMessageMap().putError("Term & courseCode", ScheduleOfClassesConstants.SOC_MSG_ERROR_NO_COURSE_OFFERING_IS_FOUND, "courseCode", courseCode, termId);
90              form.getCoDisplayWrapperList().clear();
91          }
92      }
93  
94      @Override
95      public void loadCourseOfferingsByTermAndInstructor(String termId, String instructorId, String instructorName, ScheduleOfClassesSearchForm form) throws Exception {
96  
97          ContextInfo contextInfo = ContextUtils.createDefaultContextInfo();
98  
99          // Search ID based on organizationName
100         if (instructorId == null || instructorId.isEmpty()) {
101             Map<String, String> searchCriteria = new HashMap<String, String>();
102             searchCriteria.put(KIMPropertyConstants.Person.PRINCIPAL_NAME, instructorName);
103             List<Person> instructors = getPersonService().findPeople(searchCriteria);
104             if (instructors.isEmpty()) {
105                 LOG.error("Error: Can't find any instructor for selected instructor in term: " + termId);
106                 GlobalVariables.getMessageMap().putError("Term & Instructor", ScheduleOfClassesConstants.SOC_MSG_ERROR_NO_COURSE_OFFERING_IS_FOUND, "instructor", instructorName, termId);
107                 form.getCoDisplayWrapperList().clear();
108             } else if (instructors.size() > 1) {
109                 LOG.error("Error: There is more than one instructor with the same name in term: " + termId);
110                 GlobalVariables.getMessageMap().putError("Term & Instructor", ScheduleOfClassesConstants.SOC_MSG_ERROR_MULTIPLE_INSTRUCTOR_IS_FOUND, instructorName);
111                 instructorId = null;
112                 form.getCoDisplayWrapperList().clear();
113             } else {
114                 instructorId = instructors.get(0).getPrincipalId();
115             }
116         }
117 
118         if (instructorId != null) {
119             //this is a cross service search between LPR and LUI, so it is inefficient (no join)
120             //First get all the luiIds that the instructor is teaching
121             //Only get active courses
122             List<String> luiIds = getLprService().getLuiIdsByPersonAndTypeAndState(instructorId, LprServiceConstants.INSTRUCTOR_MAIN_TYPE_KEY, LprServiceConstants.ACTIVE_STATE_KEY, contextInfo);
123 
124             List<String> courseOfferingIds = null;
125 
126             if(luiIds != null && !luiIds.isEmpty()){
127                 //Now find all the COs with Aos that are attached to that instructor.
128                 // Build a query
129                 QueryByCriteria.Builder qbcBuilder = QueryByCriteria.Builder.create();
130                 qbcBuilder.setPredicates(PredicateFactory.and(
131                     PredicateFactory.in("aoid", luiIds.toArray()),
132                     PredicateFactory.equalIgnoreCase("atpId", termId)),
133                     PredicateFactory.equal("luiState", LuiServiceConstants.LUI_CO_STATE_OFFERED_KEY));
134                 QueryByCriteria criteria = qbcBuilder.build();
135                 courseOfferingIds = getCourseOfferingService().searchForCourseOfferingIds(criteria, contextInfo);
136 
137                 if(courseOfferingIds.size() > 0){
138                     form.getCoDisplayWrapperList().clear();
139                     form.setCoDisplayWrapperList(getCourseOfferingDisplayWrappersByIds(courseOfferingIds,getCourseOfferingService(),contextInfo));
140                 }
141             }
142 
143             //If nothing was found then error
144             if(courseOfferingIds == null || courseOfferingIds.isEmpty()) {
145                 LOG.error("Error: Can't find any Course Offering for selected Instructor in term: " + termId);
146                 GlobalVariables.getMessageMap().putError("Term & Instructor", ScheduleOfClassesConstants.SOC_MSG_ERROR_NO_COURSE_OFFERING_IS_FOUND, "instructor", instructorId, termId);
147                 form.getCoDisplayWrapperList().clear();
148             }
149         }
150     }
151 
152     public void loadCourseOfferingsByTermAndDepartment(String termId, String organizationId, String organizationName, ScheduleOfClassesSearchForm form) throws Exception{
153         ContextInfo contextInfo = ContextUtils.createDefaultContextInfo();
154 
155         QueryByCriteria.Builder qbcBuilder = QueryByCriteria.Builder.create();
156 
157         // Search ID based on organizationName
158         if (organizationId == null || organizationId.isEmpty()) {
159             QueryByCriteria.Builder qBuilder = QueryByCriteria.Builder.create();
160             qBuilder.setPredicates(PredicateFactory.equalIgnoreCase("longName", organizationName));
161             QueryByCriteria query = qBuilder.build();
162             OrganizationService organizationService = getOrganizationService();
163             List<String> orgIDs = organizationService.searchForOrgIds(query, ContextUtils.createDefaultContextInfo());
164             if (orgIDs.isEmpty()) {
165                 LOG.error("Error: Can't find any Department for selected Department in term: " + termId);
166                 GlobalVariables.getMessageMap().putError("Term & Department", ScheduleOfClassesConstants.SOC_MSG_ERROR_NO_COURSE_OFFERING_IS_FOUND, "department", organizationName, termId);
167                 form.getCoDisplayWrapperList().clear();
168             } else if (orgIDs.size() > 1) {
169                 LOG.error("Error: There is more than one departments with the same long name in term: " + termId);
170                 GlobalVariables.getMessageMap().putError("Term & Department", ScheduleOfClassesConstants.SOC_MSG_ERROR_MULTIPLE_DEPARTMENT_IS_FOUND, organizationName);
171                 form.getCoDisplayWrapperList().clear();
172             }
173         } else {
174             qbcBuilder.setPredicates(PredicateFactory.and(
175                 PredicateFactory.equal("luiContentOwner", organizationId),
176                 PredicateFactory.equal("atpId", termId),
177                 PredicateFactory.equal("luiType", LuiServiceConstants.COURSE_OFFERING_TYPE_KEY),
178                 PredicateFactory.equal("luiState", LuiServiceConstants.LUI_CO_STATE_OFFERED_KEY)));
179             QueryByCriteria criteria = qbcBuilder.build();
180             List<String> courseOfferingIds = getCourseOfferingService().searchForCourseOfferingIds(criteria, contextInfo);
181 
182             if(courseOfferingIds.size() > 0){
183                 form.getCoDisplayWrapperList().clear();
184                 form.setCoDisplayWrapperList(getCourseOfferingDisplayWrappersByIds(courseOfferingIds,getCourseOfferingService(),contextInfo));
185             } else {            //If nothing was found then error
186                 LOG.error("Error: Can't find any Course Offering for selected Department in term: " + termId);
187                 GlobalVariables.getMessageMap().putError("Term & Department", ScheduleOfClassesConstants.SOC_MSG_ERROR_NO_COURSE_OFFERING_IS_FOUND, "department", organizationName, termId);
188                 form.getCoDisplayWrapperList().clear();
189             }
190         }
191     }
192 
193     public void loadActivityOfferingsByCourseOfferingId(String courseOfferingId, ScheduleOfClassesSearchForm form) throws Exception {
194 
195         ContextInfo contextInfo = ContextUtils.createDefaultContextInfo();
196 
197         List<ActivityOfferingDisplayWrapper> aoDisplayWrapperList = new ArrayList<ActivityOfferingDisplayWrapper>();
198         List<ActivityOfferingDisplayInfo> aoDisplayInfoList = getCourseOfferingService().getActivityOfferingDisplaysForCourseOffering(courseOfferingId, contextInfo);
199 
200         for (ActivityOfferingDisplayInfo aoDisplayInfo : aoDisplayInfoList) {
201             //Only returned offered AOS
202             if(LuiServiceConstants.LUI_AO_STATE_OFFERED_KEY.equals(aoDisplayInfo.getStateKey())){
203                 ActivityOfferingDisplayWrapper aoDisplayWrapper = new ActivityOfferingDisplayWrapper();
204                 aoDisplayWrapper.setAoDisplayInfo(aoDisplayInfo);
205 
206                 // Adding Information (icons)
207                 String information = "";
208                 if (aoDisplayInfo.getIsHonorsOffering() != null && aoDisplayInfo.getIsHonorsOffering()) {
209                     information = "<img src=" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HONORS_COURSE_IMG + " title=\"" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HELP_HONORS_ACTIVITY + "\"> ";
210                 }
211                 aoDisplayWrapper.setInformation(information);
212 
213                 if(aoDisplayInfo.getScheduleDisplay()!=null && !aoDisplayInfo.getScheduleDisplay().getScheduleComponentDisplays().isEmpty()){
214                     //TODO handle TBA state
215                     List<? extends ScheduleComponentDisplay> scheduleComponentDisplays = aoDisplayInfo.getScheduleDisplay().getScheduleComponentDisplays();
216                     for (ScheduleComponentDisplay scheduleComponentDisplay : scheduleComponentDisplays) {
217                         if(scheduleComponentDisplay.getBuilding() != null){
218                             aoDisplayWrapper.setBuildingName(scheduleComponentDisplay.getBuilding().getBuildingCode(), true);
219                         }
220                         if(scheduleComponentDisplay.getRoom() != null){
221                             aoDisplayWrapper.setRoomName(scheduleComponentDisplay.getRoom().getRoomCode(), true);
222                         }
223                         if(!scheduleComponentDisplay.getTimeSlots().isEmpty()){
224                             if(scheduleComponentDisplay.getTimeSlots().get(0).getStartTime() != null){
225                                 aoDisplayWrapper.setStartTimeDisplay(millisToTime(scheduleComponentDisplay.getTimeSlots().get(0).getStartTime().getMilliSeconds()), true);
226                             }
227                             if(scheduleComponentDisplay.getTimeSlots().get(0).getEndTime() != null){
228                                 aoDisplayWrapper.setEndTimeDisplay(millisToTime(scheduleComponentDisplay.getTimeSlots().get(0).getEndTime().getMilliSeconds()), true);
229                             }
230                             aoDisplayWrapper.setDaysDisplayName(getDays(scheduleComponentDisplay.getTimeSlots().get(0).getWeekdays()), true);
231                         }
232                     }
233 
234                 }
235 
236                 //  Set the instructor name
237                 aoDisplayWrapper.setInstructorDisplayNames(aoDisplayInfo.getInstructorName(), true);
238 
239                 aoDisplayWrapperList.add(aoDisplayWrapper);
240             }
241         }
242 
243         form.setAoDisplayWrapperList(aoDisplayWrapperList);
244     }
245 
246     @Override
247     public void loadCourseOfferingsByTitleAndDescription(String termId, String titleOrDescription, ScheduleOfClassesSearchForm form) throws Exception {
248         ContextInfo contextInfo = ContextUtils.createDefaultContextInfo();
249 
250         QueryByCriteria.Builder qbcBuilder = QueryByCriteria.Builder.create();
251 
252         // Note: the longName is not in the luiEntity so we need to use the criteriaLookupService is used.
253         // it is linked back to CourseOfferingCriteriaTransform and wired in the ks-enroll-context.xml
254         qbcBuilder.setPredicates(PredicateFactory.and(
255                 PredicateFactory.equal("atpId", termId),
256                 PredicateFactory.equal("luiType", LuiServiceConstants.COURSE_OFFERING_TYPE_KEY),
257                 PredicateFactory.equal("luiState", LuiServiceConstants.LUI_CO_STATE_OFFERED_KEY),
258                 PredicateFactory.and(
259                         PredicateFactory.or(
260                            PredicateFactory.like("plain", "%" + titleOrDescription + "%"), // this is for the description
261                            PredicateFactory.like("longName", titleOrDescription + "%")     // this is for the title
262                         )
263                 )
264 
265         ));
266         QueryByCriteria criteria = qbcBuilder.build();
267         List<String> courseOfferingIds = getCourseOfferingService().searchForCourseOfferingIds(criteria, contextInfo);
268 
269         if(courseOfferingIds.size() > 0){
270             form.getCoDisplayWrapperList().clear();
271             form.setCoDisplayWrapperList(getCourseOfferingDisplayWrappersByIds(courseOfferingIds,getCourseOfferingService(),contextInfo));
272         } else {    //If nothing was found then error
273             LOG.error("Error: Can't find any Course Offering for selected Department in term: " + termId);
274             GlobalVariables.getMessageMap().putError("Title & Description", ScheduleOfClassesConstants.SOC_MSG_ERROR_NO_COURSE_OFFERING_IS_FOUND, "title or description", titleOrDescription, termId);
275             form.getCoDisplayWrapperList().clear();
276         }
277 
278     }
279 
280     protected static List<CourseOfferingDisplayWrapper> getCourseOfferingDisplayWrappersByIds(List<String> courseOfferingIds, CourseOfferingService courseOfferingService, ContextInfo contextInfo) throws Exception{
281         List<CourseOfferingDisplayWrapper> coDisplayWrapperList = new ArrayList<CourseOfferingDisplayWrapper>();
282 
283         if(courseOfferingIds.size() > 0){
284 
285             List<CourseOfferingDisplayInfo> coDisplayInfoList = courseOfferingService.getCourseOfferingDisplaysByIds(courseOfferingIds, contextInfo);
286 
287             for (CourseOfferingDisplayInfo coDisplayInfo : coDisplayInfoList) {
288                 CourseOfferingDisplayWrapper coDisplayWrapper = new CourseOfferingDisplayWrapper();
289                 coDisplayWrapper.setCoDisplayInfo(coDisplayInfo);
290 
291                 // Adding Information (icons)
292                 String information = "";
293                 if (coDisplayInfo.getIsHonorsOffering() != null && coDisplayInfo.getIsHonorsOffering()) {
294                     information = "<img src=" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HONORS_COURSE_IMG + " title=\"" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HELP_HONORS_COURSE + "\"> ";
295                 }
296                 if (coDisplayInfo.getGradingOption() != null && coDisplayInfo.getGradingOption().getKey() != null
297                         && coDisplayInfo.getGradingOption().getKey().equals(LrcServiceConstants.RESULT_GROUP_KEY_GRADE_SATISFACTORY)) {
298                     information = information + "<img src=" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_GRADING_SATISFACTORY_IMG + " title=\"" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HELP_GRADING_SATISFACTORY + "\"> ";
299                 } else if (coDisplayInfo.getGradingOption() != null && coDisplayInfo.getGradingOption().getKey() != null
300                         && coDisplayInfo.getGradingOption().getKey().equals(LrcServiceConstants.RESULT_GROUP_KEY_GRADE_PERCENTAGE)) {
301                     information = information + "<img src=" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_GRADING_PERCENT_IMG + " title=\"" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HELP_GRADING_PERCENT + "\"> ";
302                 }
303                 if (!coDisplayInfo.getStudentRegistrationGradingOptions().isEmpty()) {
304                     for (KeyNameInfo stuRegOption : coDisplayInfo.getStudentRegistrationGradingOptions()) {
305                         if (stuRegOption.getKey().equals(LrcServiceConstants.RESULT_GROUP_KEY_GRADE_PASSFAIL)) {
306                             information = information + "<img src=" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_STUREG_PASSFAIL_IMG + " title=\"" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HELP_STUREG_PASSFAIL + "\">";
307                         } else if (stuRegOption.getKey().equals(LrcServiceConstants.RESULT_GROUP_KEY_GRADE_AUDIT)) {
308                             //FindBugs - it is fine as is
309                             information = information + "<img src=" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_STUREG_AUDIT_IMG + " title=\"" + ScheduleOfClassesConstants.SOC_RESULT_PAGE_HELP_STUREG_AUDIT + "\">";
310                         }
311                     }
312                 }
313                 coDisplayWrapper.setInformation(information);
314 
315                 coDisplayWrapperList.add(coDisplayWrapper);
316             }
317 
318         }
319 
320         return coDisplayWrapperList;
321     }
322 
323 
324     private String millisToTime(Long milliseconds){
325         if(milliseconds == null){
326             return null;
327         }
328         final Calendar cal = Calendar.getInstance();
329         cal.setTimeInMillis(milliseconds);
330         return DateFormatters.HOUR_MINUTE_AM_PM_TIME_FORMATTER.format(cal.getTime());
331 
332     }
333 
334 
335     private CourseOfferingService getCourseOfferingService() {
336         if (coService == null) {
337             coService = (CourseOfferingService) GlobalResourceLoader.getService(new QName(CourseOfferingServiceConstants.NAMESPACE,
338                     CourseOfferingServiceConstants.SERVICE_NAME_LOCAL_PART));
339         }
340         return coService;
341     }
342 
343     private LprService getLprService() {
344         if (lprService == null) {
345             lprService = (LprService) GlobalResourceLoader.getService(new QName(LprServiceConstants.NAMESPACE,
346                     LprServiceConstants.SERVICE_NAME_LOCAL_PART));
347         }
348         return lprService;
349     }
350 
351 
352     private OrganizationService getOrganizationService(){
353         if(organizationService == null) {
354             organizationService = (OrganizationService) GlobalResourceLoader.getService(new QName(CommonServiceConstants.REF_OBJECT_URI_GLOBAL_PREFIX + "organization", "OrganizationService"));
355         }
356         return organizationService;
357     }
358 
359     public PersonService getPersonService() {
360         return KimApiServiceLocator.getPersonService();
361     }
362 
363     private String convertIntoDaysDisplay(int day) {
364         String dayOfWeek;
365         switch (day) {
366             case 1:
367                 dayOfWeek = SchedulingServiceConstants.SUNDAY_TIMESLOT_DISPLAY_DAY_CODE;
368                 break;
369             case 2:
370                 dayOfWeek = SchedulingServiceConstants.MONDAY_TIMESLOT_DISPLAY_DAY_CODE;
371                 break;
372             case 3:
373                 dayOfWeek = SchedulingServiceConstants.TUESDAY_TIMESLOT_DISPLAY_DAY_CODE;
374                 break;
375             case 4:
376                 dayOfWeek = SchedulingServiceConstants.WEDNESDAY_TIMESLOT_DISPLAY_DAY_CODE;
377                 break;
378             case 5:
379                 dayOfWeek = SchedulingServiceConstants.THURSDAY_TIMESLOT_DISPLAY_DAY_CODE;
380                 break;
381             case 6:
382                 dayOfWeek = SchedulingServiceConstants.FRIDAY_TIMESLOT_DISPLAY_DAY_CODE;
383                 break;
384             case 7:
385                 dayOfWeek = SchedulingServiceConstants.SATURDAY_TIMESLOT_DISPLAY_DAY_CODE;
386                 break;
387             default:
388                 dayOfWeek = StringUtils.EMPTY;
389         }
390         // TODO implement TBA when service stores it.
391         return dayOfWeek;
392     }
393 
394     private String getDays(List<Integer> intList) {
395 
396         StringBuilder sb = new StringBuilder();
397         if(intList == null){
398             return sb.toString();
399         }
400 
401         for(Integer d : intList) {
402             sb.append(convertIntoDaysDisplay(d));
403         }
404 
405         return sb.toString();
406     }
407 
408 }