/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/sections/trunk/sections-app/src/java/org/sakaiproject/tool/section/jsf/backingbean/AddSectionsBean.java $
* $Id: AddSectionsBean.java 105080 2012-02-24 23:10:31Z ottenhoff@longsight.com $
***********************************************************************************
*
* 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.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);
numSectionsSelectItems = new ArrayList<SelectItem>(10);
for(int i=0; i < 10;) {
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();
}
}