Java tutorial
/********************************************************************************** * $URL$ * $Id$ *********************************************************************************** * * Copyright (c) 2005, 2006, 2007, 2008 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.tool.section.jsf.backingbean; import java.io.Serializable; import java.sql.Time; import java.text.DateFormatSymbols; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Iterator; import java.util.List; import javax.faces.context.FacesContext; import javax.faces.event.ActionEvent; import javax.faces.event.ValueChangeEvent; import javax.faces.model.SelectItem; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.component.api.ComponentManager; import org.sakaiproject.section.api.coursemanagement.Course; import org.sakaiproject.section.api.coursemanagement.CourseSection; import org.sakaiproject.tool.section.jsf.JsfUtil; import org.sakaiproject.util.ResourceLoader; /** * Controls the add sections page. * * @author <a href="mailto:jholtzman@berkeley.edu">Josh Holtzman</a> * */ public class AddSectionsBean extends CourseDependentBean implements SectionEditor, Serializable { private static final long serialVersionUID = 1L; private static final Log log = LogFactory.getLog(AddSectionsBean.class); private Integer numToAdd; private String category; private List<SelectItem> categoryItems; private List<SelectItem> numSectionsSelectItems; private List<CourseSection> sections; private String rowStyleClasses; private String elementToFocus; private transient boolean sectionsChanged; private String[] daysOfWeek = null; /** * @inheritDoc */ public void init() { if (log.isDebugEnabled()) log.debug("sections = " + sections); if (log.isDebugEnabled()) log.debug("sectionsChanged = " + sectionsChanged); ComponentManager cm = org.sakaiproject.component.cover.ComponentManager.getInstance(); ServerConfigurationService serverConfigurationService = (ServerConfigurationService) cm .get(ServerConfigurationService.class); int limit = serverConfigurationService.getInt("sections.maxgroups.category", 10); if (limit <= 0) limit = 10; numSectionsSelectItems = new ArrayList<SelectItem>(limit); for (int i = 0; i < limit;) { Integer currVal = ++i; numSectionsSelectItems.add(new SelectItem(currVal)); } if (numToAdd == null) numToAdd = 1; if (sections == null || sectionsChanged) { if (log.isDebugEnabled()) log.debug("initializing add sections bean"); List categories = getSectionCategories(); populateSections(); categoryItems = new ArrayList<SelectItem>(); for (Iterator iter = categories.iterator(); iter.hasNext();) { String cat = (String) iter.next(); categoryItems.add(new SelectItem(cat, getCategoryName(cat))); } } initDaysOfWeek(); } /** * Responds to a change in the sections selector in the UI. * * @param event */ public void processChangeNumSections(ValueChangeEvent event) { if (log.isDebugEnabled()) log.debug("processing a ui change in number of sections to add"); sectionsChanged = true; } public void processChangeSectionsCategory(ValueChangeEvent event) { if (log.isDebugEnabled()) log.debug("processing a ui change in category of sections to add"); sectionsChanged = true; } public void processAddMeeting(ActionEvent action) { if (log.isDebugEnabled()) log.debug("processing an 'add meeting' action from " + this.getClass().getName()); int index = Integer.parseInt(JsfUtil.getStringFromParam("sectionIndex")); sections.get(index).getMeetings().add(new LocalMeetingModel()); elementToFocus = action.getComponent().getClientId(FacesContext.getCurrentInstance()); } /** * Populates the section collection and row css classes. * */ private void populateSections() { if (log.isDebugEnabled()) log.debug("populating sections"); Course course = getCourse(); sections = new ArrayList<CourseSection>(); StringBuilder rowClasses = new StringBuilder(); if (StringUtils.trimToNull(category) != null) { if (log.isDebugEnabled()) log.debug("populating sections"); String categoryName = getCategoryName(category); int offset = getSectionManager().getSectionsInCategory(getSiteContext(), category).size(); for (int i = 1; i <= numToAdd; i++) { LocalSectionModel section = new LocalSectionModel(course, categoryName + (i + offset), category, null); section.getMeetings().add(new LocalMeetingModel()); sections.add(section); if (i > 1) { rowClasses.append("nextSectionRow"); } if (i < numToAdd) { rowClasses.append(","); } } rowStyleClasses = rowClasses.toString(); } } /** * Checks whether a string is currently being used as a title for another section. * * @param title * @param existingSections * @return */ private boolean isDuplicateSectionTitle(String title, Collection existingSections) { for (Iterator iter = existingSections.iterator(); iter.hasNext();) { CourseSection section = (CourseSection) iter.next(); if (section.getTitle().equals(title)) { if (log.isDebugEnabled()) log.debug("Conflicting section name found: " + title); return true; } } return false; } /** * Adds the sections, or generates validation messages for bad inputs. * * @return */ public String addSections() { if (validationFails()) { setNotValidated(true); return "failure"; } // Validation passed, so save the new sections String courseUuid = getCourse().getUuid(); StringBuilder titles = new StringBuilder(); String sepChar = JsfUtil.getLocalizedMessage("section_separator"); for (Iterator iter = sections.iterator(); iter.hasNext();) { LocalSectionModel sectionModel = (LocalSectionModel) iter.next(); titles.append(sectionModel.getTitle()); if (iter.hasNext()) { titles.append(sepChar); titles.append(" "); } } getSectionManager().addSections(courseUuid, sections); String[] params = new String[3]; params[0] = titles.toString(); if (sections.size() == 1) { params[1] = JsfUtil.getLocalizedMessage("add_section_successful_singular"); params[2] = JsfUtil.getLocalizedMessage("section_singular"); } else { params[1] = JsfUtil.getLocalizedMessage("add_section_successful_plural"); params[2] = JsfUtil.getLocalizedMessage("section_plural"); } JsfUtil.addRedirectSafeInfoMessage(JsfUtil.getLocalizedMessage("add_section_successful", params)); return "overview"; } /** * Since the validation and conversion rules rely on the *relative* * values of one component to another, we can't use JSF validators and * converters. So we check everything here. * * @return */ protected boolean validationFails() { Collection<CourseSection> existingSections = getAllSiteSections(); // Keep track of whether a validation failure occurs boolean validationFailure = false; // We also need to keep track of whether an invalid time was entered, // so we can skip the time comparisons boolean invalidTimeEntered = false; int sectionIndex = 0; for (Iterator iter = sections.iterator(); iter.hasNext(); sectionIndex++) { LocalSectionModel sectionModel = (LocalSectionModel) iter.next(); // Ensure that this title isn't being used by another section if (isDuplicateSectionTitle(sectionModel.getTitle(), existingSections)) { if (log.isDebugEnabled()) log.debug("Failed to update section... duplicate title: " + sectionModel.getTitle()); String componentId = "addSectionsForm:sectionTable:" + sectionIndex + ":titleInput"; JsfUtil.addErrorMessage(JsfUtil.getLocalizedMessage("section_add_failure_duplicate_title", new String[] { sectionModel.getTitle() }), componentId); validationFailure = true; } // Add this new section to the list of existing sections, so any other new sections don't conflict with this section's title existingSections.add(sectionModel); // Ensure that the user didn't choose to limit the size of the section without specifying a max size if (Boolean.TRUE.toString().equals(sectionModel.getLimitSize()) && sectionModel.getMaxEnrollments() == null) { String componentId = "addSectionsForm:sectionTable:" + sectionIndex + ":maxEnrollmentInput"; JsfUtil.addErrorMessage(JsfUtil.getLocalizedMessage("sections_specify_limit"), componentId); validationFailure = true; } int meetingIndex = 0; for (Iterator meetingsIterator = sectionModel.getMeetings().iterator(); meetingsIterator .hasNext(); meetingIndex++) { LocalMeetingModel meeting = (LocalMeetingModel) meetingsIterator.next(); if (!meeting.isStartTimeDefault() && isInvalidTime(meeting.getStartTimeString())) { if (log.isDebugEnabled()) log.debug("Failed to add section... meeting start time " + meeting.getStartTimeString() + " is invalid"); String componentId = "addSectionsForm:sectionTable:" + sectionIndex + ":meetingsTable:" + meetingIndex + ":startTime"; JsfUtil.addErrorMessage( JsfUtil.getLocalizedMessage("javax.faces.convert.DateTimeConverter.CONVERSION"), componentId); validationFailure = true; invalidTimeEntered = true; } if (!meeting.isEndTimeDefault() && isInvalidTime(meeting.getEndTimeString())) { if (log.isDebugEnabled()) log.debug("Failed to add section... meeting end time " + meeting.getEndTimeString() + " is invalid"); String componentId = "addSectionsForm:sectionTable:" + sectionIndex + ":meetingsTable:" + meetingIndex + ":endTime"; JsfUtil.addErrorMessage( JsfUtil.getLocalizedMessage("javax.faces.convert.DateTimeConverter.CONVERSION"), componentId); validationFailure = true; invalidTimeEntered = true; } // No need to check this if we already have invalid times if (!invalidTimeEntered && isEndTimeWithoutStartTime(meeting)) { if (log.isDebugEnabled()) log.debug("Failed to update section... start time without end time"); String componentId = "addSectionsForm:sectionTable:" + sectionIndex + ":meetingsTable:" + meetingIndex + ":startTime"; JsfUtil.addErrorMessage(JsfUtil.getLocalizedMessage("section_update_failure_end_without_start"), componentId); validationFailure = true; } if (isInvalidMaxEnrollments(sectionModel)) { if (log.isDebugEnabled()) log.debug("Failed to update section... max enrollments is not valid"); String componentId = "addSectionsForm:sectionTable:" + sectionIndex + ":maxEnrollmentInput"; JsfUtil.addErrorMessage(JsfUtil.getLocalizedMessage( "javax.faces.validator.LongRangeValidator.MINIMUM", new String[] { "0" }), componentId); validationFailure = true; } // Don't bother checking if the time values are invalid if (!invalidTimeEntered && isEndTimeBeforeStartTime(meeting)) { if (log.isDebugEnabled()) log.debug("Failed to update section... end time is before start time"); String componentId = "addSectionsForm:sectionTable:" + sectionIndex + ":meetingsTable:" + meetingIndex + ":endTime"; JsfUtil.addErrorMessage(JsfUtil.getLocalizedMessage("section_update_failure_end_before_start"), componentId); validationFailure = true; } } } return validationFailure; } /** * As part of the crutch for JSF's inability to do validation on relative * values in different components, this method checks whether an end time has * been entered without a start time. * * @param startTime * @param endTime * @return */ protected boolean isEndTimeWithoutStartTime(LocalMeetingModel meeting) { if (meeting.getStartTime() == null && meeting.getEndTime() != null) { if (log.isDebugEnabled()) log.debug("You can not set an end time without setting a start time."); return true; } return false; } /** * As part of the crutch for JSF's inability to do validation on relative * values in different components, this method checks whether two times, as * expressed by string start and end times and booleans indicating am/pm, * express times where the end time proceeds a start time. * * @param meeting * @return */ public static boolean isEndTimeBeforeStartTime(LocalMeetingModel meeting) { String startTime = null; if (!meeting.isStartTimeDefault()) { startTime = meeting.getStartTimeString(); } String endTime = null; if (!meeting.isEndTimeDefault()) { endTime = meeting.getEndTimeString(); } boolean startTimeAm = meeting.isStartTimeAm(); boolean endTimeAm = meeting.isEndTimeAm(); if (StringUtils.trimToNull(startTime) != null && StringUtils.trimToNull(endTime) != null) { Time start = JsfUtil.convertStringToTime(startTime, startTimeAm); Time end = JsfUtil.convertStringToTime(endTime, endTimeAm); if (start.after(end)) { if (log.isDebugEnabled()) log.debug("You can not set an end time earlier than the start time."); return true; } } if (StringUtils.trimToNull(startTime) != null && StringUtils.trimToNull(endTime) != null) { Time start = JsfUtil.convertStringToTime(startTime, startTimeAm); Time end = JsfUtil.convertStringToTime(endTime, endTimeAm); if (start.equals(end)) { if (log.isDebugEnabled()) log.debug("You can not set an end time that same as start time."); return true; } } return false; } /** * As part of the crutch for JSF's inability to do validation on relative * values in different components, this method checks whether a string can * represent a valid time. * * Returns true if the string fails to represent a time. Java's date formatters * allow for impossible field values (eg hours > 12) so we do manual checks here. * Ugh. * * @param str The string that might represent a time. * * @return */ protected boolean isInvalidTime(String str) { if (StringUtils.trimToNull(str) == null) { // Empty strings are ok return false; } if (str.indexOf(':') != -1) { // This is a fully specified time String[] sa = str.split(":"); if (sa.length != 2) { if (log.isDebugEnabled()) log.debug("This is not a valid time... it has more than 1 ':'."); return true; } return outOfRange(sa[0], 2, 1, 12) || outOfRange(sa[1], 2, 0, 59); } else { return outOfRange(str, 2, 1, 12); } } /** * Returns true if the string is longer than len, less than low, or higher than high. * * @param str The string * @param len The max length of the string * @param low The lowest possible numeric value * @param high The highest possible numeric value * @return */ private static boolean outOfRange(String str, int len, int low, int high) { if (str.length() > len) { return true; } try { int i = Integer.parseInt(str); if (i < low || i > high) { return true; } } catch (NumberFormatException nfe) { if (log.isDebugEnabled()) log.debug("time must be a number"); return true; } return false; } private boolean isInvalidMaxEnrollments(LocalSectionModel sectionModel) { return sectionModel.getMaxEnrollments() != null && sectionModel.getMaxEnrollments().intValue() < 0; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public int getNumToAdd() { return numToAdd; } public void setNumToAdd(int numToAdd) { this.numToAdd = numToAdd; } public List<SelectItem> getCategoryItems() { return categoryItems; } public List<SelectItem> getNumSectionsSelectItems() { return numSectionsSelectItems; } public List<CourseSection> getSections() { return sections; } public String getRowStyleClasses() { return rowStyleClasses; } public String getElementToFocus() { return elementToFocus; } public void setElementToFocus(String scrollDepth) { this.elementToFocus = scrollDepth; } public String getMonday() { return daysOfWeek[Calendar.MONDAY]; } public String getTuesday() { return daysOfWeek[Calendar.TUESDAY]; } public String getWednesday() { return daysOfWeek[Calendar.WEDNESDAY]; } public String getThursday() { return daysOfWeek[Calendar.THURSDAY]; } public String getFriday() { return daysOfWeek[Calendar.FRIDAY]; } public String getSaturday() { return daysOfWeek[Calendar.SATURDAY]; } public String getSunday() { return daysOfWeek[Calendar.SUNDAY]; } protected void initDaysOfWeek() { ResourceLoader rl = new ResourceLoader(); DateFormatSymbols dfs = new DateFormatSymbols(rl.getLocale()); daysOfWeek = dfs.getWeekdays(); } }