Index: lams_bb_integration/RELEASE_NOTES.TXT =================================================================== diff -u -rd27ed028b0e16c263776418b7bce22099fed4eed -r0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43 --- lams_bb_integration/RELEASE_NOTES.TXT (.../RELEASE_NOTES.TXT) (revision d27ed028b0e16c263776418b7bce22099fed4eed) +++ lams_bb_integration/RELEASE_NOTES.TXT (.../RELEASE_NOTES.TXT) (revision 0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43) @@ -115,4 +115,8 @@ * LDEV-3510: Previous releases were opening lessons in a new window due to a change made for LDEV-3510. Now reverted back to previous behaviour with lesson loading in the same tab. * LKC-61: Gradebook syncing change -* LDEV-3621: Ability to import and use groups from integrated server \ No newline at end of file +* LDEV-3621: Ability to import and use groups from integrated server + +1.2.18 Release Fixes +==================== +* LDEV-3399: Support Course Copy feature in Blackboard \ No newline at end of file Index: lams_bb_integration/WEB-INF/bb-manifest.xml =================================================================== diff -u -r7d945ecdc0463c75a58f16251e203068bb2e8149 -r0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43 --- lams_bb_integration/WEB-INF/bb-manifest.xml (.../bb-manifest.xml) (revision 7d945ecdc0463c75a58f16251e203068bb2e8149) +++ lams_bb_integration/WEB-INF/bb-manifest.xml (.../bb-manifest.xml) (revision 0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43) @@ -5,7 +5,7 @@ - + @@ -79,6 +79,7 @@ + Index: lams_bb_integration/build.xml =================================================================== diff -u -r7d945ecdc0463c75a58f16251e203068bb2e8149 -r0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43 --- lams_bb_integration/build.xml (.../build.xml) (revision 7d945ecdc0463c75a58f16251e203068bb2e8149) +++ lams_bb_integration/build.xml (.../build.xml) (revision 0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43) @@ -2,7 +2,7 @@ - + @@ -71,4 +71,22 @@ + + + + + + + + + + + + + + + + + Index: lams_bb_integration/conf/lams-course-copy/bb-manifest.xml =================================================================== diff -u --- lams_bb_integration/conf/lams-course-copy/bb-manifest.xml (revision 0) +++ lams_bb_integration/conf/lams-course-copy/bb-manifest.xml (revision 0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43) @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + Description + + + + + + + + + + + + + + + + + + + + + + + Index: lams_bb_integration/conf/lams-course-copy/web.xml =================================================================== diff -u --- lams_bb_integration/conf/lams-course-copy/web.xml (revision 0) +++ lams_bb_integration/conf/lams-course-copy/web.xml (revision 0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43) @@ -0,0 +1,4 @@ + + + LAMS Course Cloning Handler + Index: lams_bb_integration/src/org/lamsfoundation/ld/cxcomponent/CxComponentImpl.java =================================================================== diff -u --- lams_bb_integration/src/org/lamsfoundation/ld/cxcomponent/CxComponentImpl.java (revision 0) +++ lams_bb_integration/src/org/lamsfoundation/ld/cxcomponent/CxComponentImpl.java (revision 0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43) @@ -0,0 +1,187 @@ +package org.lamsfoundation.ld.cxcomponent; + +import org.apache.log4j.Logger; +import org.lamsfoundation.ld.integration.blackboard.GradebookServlet; +import org.lamsfoundation.ld.integration.blackboard.LamsSecurityUtil; + +import blackboard.base.BbList; +import blackboard.data.ValidationException; +import blackboard.data.content.Content; +import blackboard.data.course.CourseMembership; +import blackboard.data.navigation.CourseToc; +import blackboard.data.user.User; +import blackboard.persist.BbPersistenceManager; +import blackboard.persist.Id; +import blackboard.persist.PersistenceException; +import blackboard.persist.PkId; +import blackboard.persist.content.ContentDbLoader; +import blackboard.persist.content.ContentDbPersister; +import blackboard.persist.course.CourseDbLoader; +import blackboard.persist.course.CourseMembershipDbLoader; +import blackboard.persist.navigation.CourseTocDbLoader; +import blackboard.platform.BbServiceManager; +import blackboard.platform.cx.component.CopyControl; +import blackboard.platform.cx.component.CxComponent; +import blackboard.platform.cx.component.ExportControl; +import blackboard.platform.cx.component.GenericPackageEntry; +import blackboard.platform.cx.component.ImportControl; +import blackboard.portal.data.ExtraInfo; +import blackboard.portal.data.PortalExtraInfo; +import blackboard.portal.servlet.PortalUtil; + +public class CxComponentImpl implements CxComponent { + + private static Logger logger = Logger.getLogger(CxComponentImpl.class); + + @Override + public void doCopy(CopyControl copyControl) { + + Id destinationCourseId = copyControl.getDestinationCourseId(); + Id sourceCourseId = copyControl.getSourceCourseId(); + + try { + + BbPersistenceManager bbPm = BbServiceManager.getPersistenceService().getDbPersistenceManager(); + ContentDbLoader contentLoader = ContentDbLoader.Default.getInstance(); + CourseDbLoader cLoader = CourseDbLoader.Default.getInstance(); + CourseMembershipDbLoader courseMemLoader = CourseMembershipDbLoader.Default.getInstance(); + + // find the main teacher + BbList monitorCourseMemberships = courseMemLoader.loadByCourseIdAndRole(sourceCourseId, + CourseMembership.Role.INSTRUCTOR, null, true); + if (monitorCourseMemberships.isEmpty()) { + BbList teachingAssistantCourseMemberships = courseMemLoader + .loadByCourseIdAndRole(sourceCourseId, CourseMembership.Role.TEACHING_ASSISTANT, null, true); + monitorCourseMemberships.addAll(teachingAssistantCourseMemberships); + if (monitorCourseMemberships.isEmpty()) { + BbList courseBuilderCourseMemberships = courseMemLoader + .loadByCourseIdAndRole(sourceCourseId, CourseMembership.Role.COURSE_BUILDER, null, true); + monitorCourseMemberships.addAll(courseBuilderCourseMemberships); + } + } + // validate method parameter + if (monitorCourseMemberships.isEmpty()) { + throw new RuntimeException("There are no monitors in the course courseId=" + sourceCourseId); + } + User teacher = monitorCourseMemberships.get(0).getUser(); + + // get a CourseTOC (Table of Contents) loader. We will need this to iterate through all of the "areas" + // within the course + CourseTocDbLoader cTocDbLoader = (CourseTocDbLoader) bbPm.getLoader(CourseTocDbLoader.TYPE); + BbList courseTocs = cTocDbLoader.loadByCourseId(destinationCourseId); + + System.out.println("Starting clonning course (courseId="+sourceCourseId + ")."); + + // iterate through the course TOC items + for (CourseToc courseToc : courseTocs) { + + // determine if the TOC item is of type "CONTENT" rather than applicaton, or something else + if ((courseToc.getTargetType() == CourseToc.Target.CONTENT) + && (courseToc.getContentId() != Id.UNSET_ID)) { + // we have determined that the TOC item is content, next we need to load the content object and + // iterate through it + // load the content tree into an object "content" and iterate through it + BbList contents = contentLoader.loadListById(courseToc.getContentId()); + // iterate through the content items in this content object + for (Content content : contents) { + // only LAMS content + if ("resource/x-lams-lamscontent".equals(content.getContentHandler())) { + String lsId = content.getLinkRef(); + + String courseIdReadable = cLoader.loadById(destinationCourseId).getCourseId(); + + // update lesson id + final Long newLessonId = LamsSecurityUtil.cloneLesson(teacher, courseIdReadable, lsId); + content.setLinkRef(Long.toString(newLessonId)); + + // update URL + String contentUrl = content.getUrl(); + contentUrl = replaceParameterValue(contentUrl, "lsid", Long.toString(newLessonId)); + + PkId coursePkId = (PkId) destinationCourseId; + String course_id = "_" + coursePkId.getPk1() + "_" + coursePkId.getPk2(); + contentUrl = replaceParameterValue(contentUrl, "course_id", course_id); + + PkId contentPkId = (PkId) content.getId(); + String content_id = "_" + contentPkId.getPk1() + "_" + contentPkId.getPk2(); + contentUrl = replaceParameterValue(contentUrl, "content_id", content_id); + content.setUrl(contentUrl); + + // persist updated content + ContentDbPersister persister = (ContentDbPersister) bbPm + .getPersister(ContentDbPersister.TYPE); + persister.persist(content); + + // store internalContentId -> externalContentId. It's used for lineitem removal (delete.jsp) + PortalExtraInfo pei = PortalUtil.loadPortalExtraInfo(null, null, "LamsStorage"); + ExtraInfo ei = pei.getExtraInfo(); + ei.setValue(content_id, Long.toString(newLessonId)); + PortalUtil.savePortalExtraInfo(pei); + + // Gradebook column will be copied automatically if appropriate option is selected on + // cloning lesson page + + System.out.println("Lesson (lessonId="+lsId + ") was successfully cloned to the one (lessonId="+newLessonId+")."); + } + + } + } + } + + } catch (PersistenceException e) { + throw new RuntimeException(e); + } catch (ValidationException e) { + throw new RuntimeException(e); + } + + System.out.println("Course (courseId="+sourceCourseId + ") was successfully cloned to the new one (courseId="+destinationCourseId+")."); + + } + + @Override + public void doExport(ExportControl exportControl) { + } + + @Override + public void doImport(GenericPackageEntry entry, ImportControl importControl) { + } + + public String getName() { + return "Sample Content"; + } + + public String getComponentHandle() { + return "lams-sample-content"; + } + + public Usage getUsage() { + return Usage.CONFIGURABLE; + } + + public String getApplicationHandle() { + return "lams-copy-archive"; + } + + @Override + public void afterImportContent(Content arg0, ImportControl arg1) { + } + + private static String replaceParameterValue(String url, String paramName, String newParamValue) { + String oldParamValue = ""; + + int quotationMarkIndex = url.indexOf("?"); + String queryPart = quotationMarkIndex > -1 ? url.substring(quotationMarkIndex + 1) : url; + String[] paramEntries = queryPart.split("&"); + for (String paramEntry : paramEntries) { + String[] paramEntrySplitted = paramEntry.split("="); + if ((paramEntrySplitted.length > 1) && paramName.equalsIgnoreCase(paramEntrySplitted[0])) { + oldParamValue = paramEntrySplitted[1]; + + return url.replaceFirst(paramName + "=" + oldParamValue, paramName + "=" + newParamValue); + } + } + + return url; + } + +} Index: lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java =================================================================== diff -u -r744501dd7374ab97efc759cb3aae74eb23ea3979 -r0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43 --- lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java (.../LamsSecurityUtil.java) (revision 744501dd7374ab97efc759cb3aae74eb23ea3979) +++ lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java (.../LamsSecurityUtil.java) (revision 0bb440cf6cd13fdca5e7792b1cfa1aed5bbcde43) @@ -562,6 +562,84 @@ } /** + * Clones lessons in lams through a LAMS webservice using the lsID & courseId parameter. + * + * @param courseId + * courseId as a request parameter + * @param ldId + * the learning design id for which you wish to start a lesson + * + * @return lesson id of a cloned lesson + */ + public static Long cloneLesson(User teacher, String courseId, String lsId) { + + String serverId = getServerID(); + String serverAddr = getServerAddress(); + String serverKey = getServerKey(); + String username = teacher.getUserName(); + String locale = teacher.getLocale(); + String country = getCountry(locale); + String lang = getLanguage(locale); + + if (courseId == null || serverId == null || serverAddr == null || serverKey == null) { + logger.info("Unable to clone lesson, one or more lams configuration properties or the course id is null"); + throw new RuntimeException("Unable to clone lesson, one or more lams configuration properties or the course id is null. courseId="+courseId); + } + + try { + String method = "clone"; + String timestamp = new Long(System.currentTimeMillis()).toString(); +// String username = "not_available";//signifier for skipping security check as we can't get the current user requested cloning + String hash = generateAuthenticationHash(timestamp, username, serverId); + String serviceURL = serverAddr + "/services/xml/LessonManager?" + "serverId=" + + URLEncoder.encode(serverId, "utf8") + "&datetime=" + timestamp + "&username=" + + URLEncoder.encode(username, "utf8") + "&hashValue=" + hash + "&courseId=" + + URLEncoder.encode(courseId, "UTF8") + "&country=" + + country + "&lang=" + lang + "&lsId=" + lsId + "&method=" + method; + + logger.info("LAMS clone lesson request: " + serviceURL); + + // InputStream is = url.openConnection().getInputStream(); + InputStream is = LamsSecurityUtil.callLamsServerPost(serviceURL); + // parse xml response + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document document = db.parse(is); + + // get the lesson id from the response + + /* + * The getTextContext is not a java 1.4 method, so Blackboard 7.1 comes up with errors using getNodeValue() + * instead + */ + // return Long.parseLong(document.getElementsByTagName("Lesson").item(0).getAttributes().getNamedItem("lessonId").getTextContent()); + return Long.parseLong(document.getElementsByTagName("Lesson").item(0).getAttributes() + .getNamedItem("lessonId").getNodeValue()); + } catch (MalformedURLException e) { + throw new RuntimeException("Unable to clone LAMS lesson, bad URL: '" + serverAddr + + "', please check lams.properties", e); + } catch (IllegalStateException e) { + throw new RuntimeException( + "LAMS Server timeout, did not get a response from the LAMS server. Please contact your systems administrator", + e); + } catch (RemoteException e) { + throw new RuntimeException("Unable to clone LAMS lesson, RMI Remote Exception", e); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unable to clone LAMS lesson, Unsupported Encoding Exception", e); + } catch (ConnectException e) { + throw new RuntimeException( + "LAMS Server timeout, did not get a response from the LAMS server. Please contact your systems administrator", + e); + } catch (IOException e) { + throw new RuntimeException("Unable to clone LAMS lesson. " + e.getMessage() + + " Please contact your system administrator.", e); + } catch (Exception e) { + throw new RuntimeException("Unable to clone LAMS lesson. Please contact your system administrator.", e); + } + + } + + /** * Pre-adding students and monitors to a lesson * * @param ctx