/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package ee.tlu.htk.dippler.course; import ee.tlu.htk.dippler.backoffice.StatusCodes; import ee.tlu.htk.dippler.entities.Assignment; import ee.tlu.htk.dippler.entities.Category; import ee.tlu.htk.dippler.entities.Course; import ee.tlu.htk.dippler.entities.Coursegroup; import ee.tlu.htk.dippler.entities.Image; import ee.tlu.htk.dippler.entities.Learner; import ee.tlu.htk.dippler.entities.LearningOutcome; import ee.tlu.htk.dippler.entities.LearningResource; import ee.tlu.htk.dippler.entities.Organization; import ee.tlu.htk.dippler.entities.ResourceFolder; import ee.tlu.htk.dippler.entities.User; import ee.tlu.htk.dippler.managers.CategoryManagerLocal; import ee.tlu.htk.dippler.managers.TagManagerLocal; import ee.tlu.htk.dippler.managers.UserManagerLocal; import ee.tlu.htk.dippler.utils.permissionChecker; import java.io.InputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * * @author metz */ enum CourseActions { CREATE, DELETE, EDIT, LOAD, LOAD_RECENT, LOAD_FEATURED, LOAD_LATEST, LOAD_ASSIGNMENTS_WITH_ANSWERS, APPROVE, UNAPPROVE, CREATE_GROUP, CREATE_ASSIGNMENT, IS_LEARNER, IS_ACTIVE_LEARNER, FIND_COURSES_LIKE, BULK_DELETE_GROUPS, BULK_ACTIVATE_LEARNERS, BULK_REMOVE_LEARNERS, BULK_DELETE_OUTCOMES, BULK_DELETE_RESOURCES, BULK_DELETE_ASSIGNMENTS, BULK_DELETE_CATEGORIES, BULK_APPROVE_COURSES, REORDER_RESOURCE_FOLDERS, REORDER_LEARNING_RESOURCES } @Stateless public class CourseManager implements CourseManagerLocal { @PersistenceContext private EntityManager em; @EJB private UserManagerLocal userManager; @EJB private GroupManagerLocal groupManager; @EJB private AssignmentManagerLocal assignmentManager; @EJB private BlogManagerLocal blogManager; @EJB private AnalyticsManagerLocal analyticsManager; @EJB private ActivityManagerLocal activityManager; @EJB private TagManagerLocal tagManager; @EJB private LearnerManagerLocal learnerManager; @EJB private OutcomeManagerLocal outcomeManager; @EJB private ResourceManagerLocal resourceManager; @EJB private CategoryManagerLocal categoryManager; public static final Integer COURSE_LOAD = 1; public static final Integer COURSE_CREATE = 50; public static final Integer COURSE_EDIT = 50; public static final Integer COURSE_DELETE = 50; public static final Integer COURSE_APPROVE = 90; public static final Integer COURSE_GROUP_CREATE = 10; public static final Integer COURSE_ASSIGNMENT_CREATE = 50; public static final Integer COURSE_IS_LEARNER = 1; public static final Integer LOAD_RECENT = 1; public static final Integer LOAD_FEATURED = 1; public static final Integer LOAD_LATEST = 1; public static final Integer LOAD_ASSIGNMENTS_WITH_ANSWERS = 50; public static final Integer BULK_DELETE = 50; //Store Unmarshaller and Context in class variable, this way we can always reuse them //Saves us 300 milliseconds (On macbook 7,1) with every request that uses unmarshaller!!!!!! private static Unmarshaller unmarshaller = null; //TODO CHECK FOR NULL FROM NEW UNMARSHAL METHOD SOLUTION public CourseManager() { try { final JAXBContext context = JAXBContext.newInstance(Course.class); unmarshaller = context.createUnmarshaller(); } catch (JAXBException ex) { Logger.getLogger(CourseManager.class.getName()).log(Level.SEVERE, null, ex); } } @Override public String manageCourse(String action, String data, User user, Organization org) { switch (CourseActions.valueOf(action)) { case CREATE: if (userManager.hasPermission(user, COURSE_CREATE)) { return this.createCourse(user, org, data); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for creating course"); } case EDIT: if (userManager.hasPermission(user, COURSE_EDIT)) { return this.editCourse(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for edit course"); } case DELETE: if (userManager.hasPermission(user, COURSE_DELETE)) { return this.deleteCourse(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for delete course"); } case APPROVE: if (userManager.hasPermission(user, COURSE_APPROVE)) { return this.approveCourse(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for approve course"); } case UNAPPROVE: if (userManager.hasPermission(user, COURSE_APPROVE)) { return this.unapproveCourse(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for unapprove course"); } case LOAD: if (userManager.hasPermission(user, COURSE_LOAD)) { return this.loadCourse(data); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for load course"); } case FIND_COURSES_LIKE: if (userManager.hasPermission(user, COURSE_LOAD)) { return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", this.findCoursesLike(data, user)); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for find courses like"); } case LOAD_RECENT: if (userManager.hasPermission(user, LOAD_RECENT)) { return this.loadRecentCourses(org); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for load recent course"); } case LOAD_FEATURED: if (userManager.hasPermission(user, LOAD_FEATURED)) { return this.loadFeaturedCourses(org); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for load featured course"); } case LOAD_LATEST: if (userManager.hasPermission(user, LOAD_LATEST)) { return this.loadCourseLatestInfo(data); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for load course latest info"); } case LOAD_ASSIGNMENTS_WITH_ANSWERS: if (userManager.hasPermission(user, LOAD_ASSIGNMENTS_WITH_ANSWERS)) { // madis - fixed spelling of Assignments in method return this.loadAssignmentsWithAnswers(data); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for load course assignements with answers"); } case CREATE_GROUP: if (userManager.hasPermission(user, COURSE_GROUP_CREATE)) { return this.createCoursegroup(user, data); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for create group"); } case CREATE_ASSIGNMENT: if (userManager.hasPermission(user, COURSE_ASSIGNMENT_CREATE)) { return this.createAssignment(user, data, org); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for create assignment"); } case IS_LEARNER: if (userManager.hasPermission(user, COURSE_IS_LEARNER)) { return this.isLearner(data, false); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is learner"); } case IS_ACTIVE_LEARNER: if (userManager.hasPermission(user, COURSE_IS_LEARNER)) { return this.isLearner(data, true); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is active learner"); } case BULK_DELETE_GROUPS: if (userManager.hasPermission(user, BULK_DELETE)) { return this.bulkDeleteGroups(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is bulk delete groups"); } case BULK_ACTIVATE_LEARNERS: if (userManager.hasPermission(user, BULK_DELETE)) { return this.bulkActivateLearners(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is bulk activate learners"); } case BULK_REMOVE_LEARNERS: if (userManager.hasPermission(user, BULK_DELETE)) { return this.bulkRemoveLearners(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is bulk remove learners"); } case BULK_DELETE_OUTCOMES: if (userManager.hasPermission(user, BULK_DELETE)) { return this.bulkDeleteLearningOutcomes(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is bulk delete outcomes"); } case BULK_DELETE_RESOURCES: if (userManager.hasPermission(user, BULK_DELETE)) { return this.bulkDeleteResources(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is bulk delete resources"); } case BULK_DELETE_ASSIGNMENTS: if (userManager.hasPermission(user, BULK_DELETE)) { return this.bulkDeleteAssignments(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is bulk delete assignments"); } case BULK_DELETE_CATEGORIES: if (userManager.hasPermission(user, BULK_DELETE)) { return this.bulkDeleteCategories(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not rights for is bulk delete categories"); } case REORDER_RESOURCE_FOLDERS: if (userManager.hasPermission(user, COURSE_EDIT)) { return this.reorderResourceFolders(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for folders reorder"); } case REORDER_LEARNING_RESOURCES: if (userManager.hasPermission(user, COURSE_EDIT)) { return this.reorderLearningResources(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights for folders reorder"); } case BULK_APPROVE_COURSES: if (userManager.hasPermission(user, COURSE_APPROVE)) { return this.bulkApproveCourses(data, user); } else { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "No rights bulk approve courses"); } default: return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, ""); } } public String findCoursesLike(String data, User user) { StringBuilder xml = new StringBuilder(); try { //c.enrollment should TINYINT not a damn CHAR Query listing = em.createNativeQuery("SELECT * FROM Course c WHERE c.approved AND c.status = '1' " + "AND c.enrollment != '0' AND ( c.title LIKE ?keyword OR c.identifier LIKE ?keyword ) AND " + "(SELECT count(course) FROM Learner l WHERE l.learner=?learner_id AND l.course=c.id)<1", Course.class); listing.setParameter("learner_id", user.getId()); listing.setParameter("keyword", "%"+data+"%"); List courses = (List) listing.getResultList(); for(Course course : courses ){ if ( course.canApply() ) { //Yes, it would be nicer to this in SQL query, but this is easier for now xml.append("").append(course.getId()).append(""); } } } catch (Exception e) { } return xml.toString(); } public String createCourse(User user, Organization org, String data) { System.out.println("CREATE COURSE"); Course course = unMarshalCourse(data); if ( course != null ) { course.setOwner(user); course.setCreator(user); course.setOrganization(org); course.setApproved(false); course.setTrashed(false); course.addFacilitator(user); //New facilitator gets added via Course relation to facilitator + Cascade.PERSIST em.persist(course); activityManager.addActivity("CREATE", course, user, course.getId(), course.getTitle(), "Course"); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", marshalCourse(course)); } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Create failed"); } public String editCourse(String data, User user) { System.out.println(data); Course cd = unMarshalCourse(data); if ( cd != null ) { Course course = findById(cd.getId()); if ( course != null ) { if (!permissionChecker.isFacilitator(user, course)) { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not owner or admin"); } course.setTitle(cd.getTitle()); course.setIdentifier(cd.getIdentifier()); course.setCredits(cd.getCredits()); course.setProvider(cd.getProvider()); course.setDescription(cd.getDescription()); course.setStatus(cd.getStatus()); course.setCloses(cd.getCloses()); course.setTag(cd.getTag()); Image course_image = new Image(); //Check if current image set, if is - remove Image current_image = course.getImage(); if ( current_image != null ) { if (current_image.getId() > 0) { if (cd.course_image.getDelete() || (cd.course_image.getFilesize() != null && !cd.course_image.getFilesize().equals(course.getImage().getFilesize())) ) { Image cim = course.getImage(); em.remove(cim); course.setImage(null); } } } //Add new image if (course.getImage() == null && cd.course_image.getName() != null) { course_image.setCreator(cd.course_image.getCreator()); course_image.setName(cd.course_image.getName()); course_image.setFiletype(cd.course_image.getFiletype()); course_image.setImage(cd.course_image.getImage()); course_image.setFilesize(cd.course_image.getFilesize()); course_image.setTrashed(false); course.setImage(course_image); //This will get CASCADE_PERSISTED } course.setAssessment(cd.getAssessment()); course.setPrerequisite(cd.getPrerequisite()); course.setAttendancemode(cd.getAttendancemode()); course.setObjective(cd.getObjective()); course.setEducationlevel(cd.getEducationlevel()); course.setLanguageofinstruction(cd.getLanguageofinstruction()); course.setEnrollment(cd.getEnrollment()); course.setEnd(cd.getEnd()); course.setStart(cd.getStart()); course.setPermission(cd.getPermission());; activityManager.addActivity("EDIT", course, user, course.getId(), course.getTitle(), "Course"); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", marshalCourse(course)); } } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Edit failed"); } public String deleteCourse(String data, User user) { Course course = findByData(data); if ( course != null ) { if (!permissionChecker.isOwnerOrAdmin(user, course.getOwner())) { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not owner or admin"); } activityManager.addActivity("DELETE", course, user, course.getId(), course.getTitle(), "Course"); em.remove(course); return StatusCodes.respond(StatusCodes.SUCCESS, ""); } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Delete failed"); } public String approveCourse(String data, User user) { Course course = findByData(data); if ( course != null ) { course.setApproved(true); activityManager.addActivity("APPROVE", course, user, course.getId(), course.getTitle(), "Course"); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", marshalCourse(course)); } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Not approved"); } public String bulkApproveCourses(String data, User user) { try { Integer success = 0; Integer failure = 0; Integer count = 0; Document doc = this.getXMLDoc(data); NodeList courses = doc.getElementsByTagName("course"); for (int i=0; i"); Query listing = em.createNativeQuery("SELECT * FROM Course c WHERE c.approved AND c.organization=?1 ORDER BY created DESC", Course.class); listing.setParameter(1, org.getId().toString()); listing.setFirstResult(0); listing.setMaxResults(4); try { List courses = (List) listing.getResultList(); for(Course course : courses ){ xml.append(marshalCourse(course)); } xml.append(""); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", xml.toString()); } catch (Exception e) { e.printStackTrace(System.out); } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Not loaded"); } public String loadFeaturedCourses(Organization org) { StringBuilder xml = new StringBuilder(""); Query listing = em.createNativeQuery("SELECT * FROM Course c WHERE c.approved AND c.status='1' AND c.organization=?1 ORDER BY created DESC", Course.class); listing.setParameter(1, org.getId().toString()); listing.setFirstResult(0); listing.setMaxResults(5); try { List courses = (List) listing.getResultList(); for(Course course : courses ){ xml.append(marshalCourse(course)); } xml.append(""); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", xml.toString()); } catch (Exception e) { e.printStackTrace(System.out); } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Not loaded"); } public String loadCourseLatestInfo(String data) { Course course = findByData(data); if ( course != null ) { StringBuilder xml = new StringBuilder(""); xml.append(assignmentManager.loadLatestInfo(course)); xml.append(blogManager.loadLatestInfo(course)); xml.append(analyticsManager.loadLatestInfo(course)); xml.append(""); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", xml.toString()); } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Not loaded"); } //TODO, after fixing GROUPMANAGER unmarshal public String createCoursegroup(User user, String data) { Coursegroup group = groupManager.unMarshal(data); if ( group != null ) { group.setCreator(user); Long course_id = group.getCourseId(); Course course = findById(course_id); if ( course != null ) { if (!(permissionChecker.isFacilitator(user, course) || isLearner(user, course))) { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not owner or admin"); } group.setCourse(course); group.setMembers(groupManager.editMembers(group)); em.persist(group); activityManager.addActivity("CREATE", course, user, group.getId(), group.getTitle(), "Coursegroup"); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", groupManager.marshal(group)); } } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Create group failed"); } //TODO after assingment manager unmarshal issue public String createAssignment(User user, String data, Organization org) { Assignment assignment = assignmentManager.unMarshal(data); if ( assignment != null ) { assignment.setCreator(user); Long course_id = assignment.getCourseId(); Course course = findById(course_id); if ( course != null ) { if (!permissionChecker.isFacilitator(user, course)) { return StatusCodes.respond(StatusCodes.OPERATION_NOT_ALLOWED, "Not owner or admin"); } assignment.setCourse(course); assignment.setTargetGroup(assignmentManager.editTargetGroup(assignment)); em.persist(assignment); tagManager.manageTags(assignment.tags, assignment, org); activityManager.addActivity("CREATE", assignment.getCourse(), user, assignment.getId(), assignment.getTitle(), "Assignment"); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", assignmentManager.marshal(assignment)); } } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Create assignment failed"); } public String loadAssignmentsWithAnswers(String data) { StringBuilder xml = new StringBuilder(""); Course course = findByData(data); if ( course != null ) { List learners = (List) course.getLearners(); List assignments = (List) course.getAssignments(); xml.append(""); for(Assignment assignment : assignments ){ xml.append(assignmentManager.answerXML(course, assignment, learners)); } xml.append(""); } xml.append(""); return StatusCodes.respondWithData(StatusCodes.SUCCESS, "", xml.toString()); } public static String loadCourseImage(Image image) { if ( image != null ) { StringBuilder xml = new StringBuilder(); xml.append("").append(image.getName()).append(""); xml.append("").append(image.getId()).append(""); xml.append("").append(image.getFiletype()).append(""); xml.append("").append(image.getFilesize()).append(""); return xml.toString(); } return ""; } public String isLearner(String data, boolean force_active) { //TODO do something with this XML parsing. Document doc = this.getXMLDoc(data); Long user_id = Long.parseLong(this.getNodeValue(doc, "user_id", true)); String course_id = this.getNodeValue(doc, "course", true); Course course = findById(Long.parseLong(course_id)); Learner learner = course.getIsLearner(user_id); System.out.println("checking if learner "+learner); if ( learner != null ) { if (force_active) { if (learner.getStatus() == 5) { return StatusCodes.respond(StatusCodes.SUCCESS, "is active learner"); } } else { return StatusCodes.respondWithData(StatusCodes.SUCCESS, "learner info", ""+learner.getStatus().toString()+""); } } return StatusCodes.respond(StatusCodes.COURSE_ERROR, "Not learner"); } @Override public Boolean isLearner(User user, Course course) { if ( course != null ) { Learner learner = course.getIsLearner(user.getId()); if ( learner != null ) { if (learner.getStatus() == 5) { return Boolean.TRUE; } } } return Boolean.FALSE; } //TODO later check these bulk methods public String bulkDeleteGroups(String data, User user) { try { Integer success = 0; Integer failure = 0; Integer count = 0; Document doc = this.getXMLDoc(data); Course course = findById(this.getLongNodeValue(doc, "course")); NodeList coursegroups = doc.getElementsByTagName("coursegroup"); for (int i=0; i 0 ) { return em.find(Course.class, id); } return null; } @Override public Course findByData(String data) { Course fakeCourse = unMarshalCourse(data); if ( fakeCourse != null ) { return findById(fakeCourse.getId()); } return null; } public static Course unMarshalCourse(String data) { if (unmarshaller != null) { try { final Course courseUNM = (Course) unmarshaller.unmarshal(new StringReader(data)); return courseUNM; } catch(JAXBException e) { //Something went wrong } } return null; } private Document getXMLDoc( String data ) { Document doc = null; try { InputStream is = new ByteArrayInputStream(data.getBytes("UTF-8")); DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); doc = builder.parse(is); } catch (ParserConfigurationException ex) { Logger.getLogger(CourseManager.class.getName()).log(Level.SEVERE, null, ex); } catch (SAXException e) { //Something went wrong } catch (IOException e) { //Something went wrong } return doc; } private String getNodeValue(Document doc, String node, boolean firstChild) { if ( !firstChild ) { return doc.getElementsByTagName(node).item(0).getTextContent(); } return doc.getElementsByTagName(node).item(0).getFirstChild().getTextContent(); } private Long getLongNodeValue(Document doc, String node) { try { return Long.parseLong(doc.getElementsByTagName(node).item(0).getTextContent()); } catch(NumberFormatException e) { //Was not a number } return 0L; } @Override public String marshalCourse(Course course) { StringBuilder xml = new StringBuilder(""); xml.append("").append(course.getId()).append(""); xml.append("<![CDATA[").append(course.getTitle()).append("]]>"); xml.append(""); xml.append("").append(course.getOwner().getId()).append(""); xml.append("").append(course.getCreator().getId()).append(""); xml.append("").append(course.getStart()).append(""); xml.append("").append(course.getEnd()).append(""); xml.append("").append(course.getCloses()).append(""); xml.append("").append(course.getProvider()).append(""); xml.append("").append(course.getIdentifier()).append(""); xml.append("").append(course.getCredits()).append(""); xml.append("").append(course.getPermission()).append(""); xml.append("").append(course.getEnrollment()).append(""); xml.append("").append(course.getApproved()).append(""); xml.append("").append(course.getEducationlevel()).append(""); xml.append("").append(course.getLanguageofinstruction()).append(""); xml.append("").append(course.getAttendancemode()).append(""); xml.append("").append(course.getObjective()).append(""); xml.append("").append(course.getAssessment()).append(""); xml.append("").append(course.getPrerequisite()).append(""); xml.append("").append(course.getTag()).append(""); xml.append("").append(loadCourseImage(course.getImage())).append(""); xml.append("").append(course.getStatus()).append(""); xml.append(""); return xml.toString(); } }