/** * OLAT - Online Learning and Training<br> * http://www.olat.org * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Copyright (c) since 2004 at Multimedia- & E-Learning Services (MELS),<br> * University of Zurich, Switzerland. * <hr> * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * This file has been modified by the OpenOLAT community. Changes are licensed * under the Apache 2.0 license as the original file. * <p> */ package org.olat.ims.cp; import java.util.Iterator; import java.util.Vector; import java.util.logging.Logger; import org.dom4j.tree.DefaultDocument; import org.dom4j.tree.DefaultElement; import org.olat.core.logging.OLATRuntimeException; import org.olat.core.util.vfs.VFSContainer; import org.olat.ims.cp.objects.CPDependency; import org.olat.ims.cp.objects.CPFile; import org.olat.ims.cp.objects.CPItem; import org.olat.ims.cp.objects.CPManifest; import org.olat.ims.cp.objects.CPMetadata; import org.olat.ims.cp.objects.CPOrganization; import org.olat.ims.cp.objects.CPOrganizations; import org.olat.ims.cp.objects.CPResource; import org.olat.ims.cp.objects.CPResources; import org.olat.modules.wiki.WikiToCPExport; /** * * Description:<br> * This class provides basic functionality for a IMS Content Package * * <P> * Initial Date: 27.06.2008 <br> * * @author Sergio Trentini */ public class CPCore { /** * The CP Manifest name */ public static final String MANIFEST_NAME = "imsmanifest.xml"; // Element and Attribute Names public static final String MANIFEST = "manifest"; public static final String ORGANIZATIONS = "organizations"; public static final String RESOURCES = "resources"; public static final String DEFAULT = "default"; public static final String ORGANIZATION = "organization"; public static final String ITEM = "item"; public static final String PARAMETERS = "parameters"; public static final String RESOURCE = "resource"; public static final String BASE = "base"; public static final String FILE = "file"; public static final String TYPE = "type"; public static final String HREF = "href"; public static final String METADATA = "metadata"; public static final String IDENTIFIER = "identifier"; public static final String IDENTIFIERREF = "identifierref"; public static final String STRUCTURE = "structure"; public static final String TITLE = "title"; public static final String DEPENDENCY = "dependency"; public static final String VERSION = "version"; public static final String SCHEMA = "schema"; public static final String SCHEMALOCATION = "schemaLocation"; public static final String SCHEMAVERSION = "schemaversion"; public static final String ISVISIBLE = "isvisible"; public static final String OLAT_MANIFEST_IDENTIFIER = "olat_ims_cp_editor_v1"; public static final String OLAT_ORGANIZATION_IDENTIFIER = "TOC"; private DefaultDocument doc; private VFSContainer rootDir; private CPManifest rootNode; private Vector<String> errors; public CPCore(DefaultDocument doc, VFSContainer rootDir) { this.doc = doc; this.rootDir = rootDir; errors = new Vector<String>(); buildTree(); } /** * parses the document, builds manifest-datamodel-tree-structure */ public void buildTree() { if (doc != null) { rootNode = new CPManifest(this, (DefaultElement) doc.getRootElement()); rootNode.buildChildren(); } } /** * * this is case-sensitive! * * @param identifier * @return an Element by its IDENTIFIER attribute starting at the manifests * root element This will do a deep recursive search */ public DefaultElement getElementByIdentifier(String identifier) { return rootNode.getElementByIdentifier(identifier); } // *** CP manipulation *** /** * adds an element as a child to the element with id parentId if the element * with parentId is not found, it returns false * * if adding was successful, it returns true */ public boolean addElement(DefaultElement newElement, String parentId, int position) { DefaultElement parentElement = rootNode.getElementByIdentifier(parentId); if (parentElement == null) { throw new OLATRuntimeException(CPOrganizations.class, "Parent-element with identifier:\"" + parentId + "\" not found!", new Exception()); } if (parentElement instanceof CPItem) { // parent is a <item> if (newElement instanceof CPItem) { // only CPItems can be added to CPItems CPItem item = (CPItem) parentElement; item.addItemAt((CPItem) newElement, position); return true; } else { throw new OLATRuntimeException(CPOrganizations.class, "you can only add <item> elements to an <item>-element", new Exception()); } } else if (parentElement instanceof CPOrganization) { // parent is a <organization> if (newElement instanceof CPItem) { // add a new item to organization element CPOrganization org = (CPOrganization) parentElement; org.addItemAt((CPItem) newElement, position); return true; } else { throw new OLATRuntimeException(CPOrganizations.class, "you can only add <item> elements to an <organization>-element", new Exception()); } } else if (parentElement instanceof CPResource) { // parent is a <resource> CPResource resource = (CPResource) parentElement; if (newElement instanceof CPFile) { resource.addFile((CPFile) newElement); } else if (newElement instanceof CPDependency) { resource.addDependency((CPDependency) newElement); } else { throw new OLATRuntimeException(CPOrganizations.class, "you can only add <dependency> or <file> elements to a Resource", new Exception()); } return true; } else if (parentElement instanceof CPResources) { // parent is <resources> !!see the "s" at the end ;) if (newElement instanceof CPResource) { CPResources resources = (CPResources) parentElement; resources.addResource((CPResource) newElement); return true; } else { throw new OLATRuntimeException(CPOrganizations.class, "you can only add <resource>elements to the <resources> element", new Exception()); } } return false; } /** * adds an element to the CP. Only accepts <resource> and <organization> * elements * * @param newElement * @return */ public void addElement(DefaultElement newElement) { if (newElement instanceof CPResource) { rootNode.getResources().addResource((CPResource) newElement); } else if (newElement instanceof CPOrganization) { rootNode.getOrganizations().addOrganization((CPOrganization) newElement); } else if (newElement instanceof CPItem) { if (rootNode.getOrganizations().getOrganizations().size() > 0) { rootNode.getOrganizations().getOrganizations().elementAt(0).addItem((CPItem) newElement); } } else { throw new OLATRuntimeException(CPOrganizations.class, "invalid newElement for adding to manifest", new Exception()); } } /** * adds an element to the cp. Adds it after the item with identifier "id" * * @param newElement * @param id * @return */ public boolean addElementAfter(DefaultElement newElement, String id) { DefaultElement beforeElement = rootNode.getElementByIdentifier(id); if (beforeElement == null) return false; if (beforeElement instanceof CPItem) { // beforeElement is a <item> // ==> newElement has to be an <item> CPItem beforeItem = (CPItem) beforeElement; DefaultElement parent = beforeItem.getParentElement(); if (!(newElement instanceof CPItem)) { throw new OLATRuntimeException(CPOrganizations.class, "only <item> element allowed", new Exception()); } if (parent instanceof CPItem) { CPItem p = (CPItem) parent; p.addItemAt((CPItem) newElement, beforeItem.getPosition() + 1); } else if (parent instanceof CPOrganization) { CPOrganization o = (CPOrganization) parent; o.addItemAt((CPItem) newElement, beforeItem.getPosition() + 1); } else { throw new OLATRuntimeException(CPOrganizations.class, "you cannot add an <item> element to a " + parent.getName() + " element", new Exception()); } } return true; } /** * removes an element with identifier "identifier" from the manifest * * @param identifier the identifier if the element to remove * @param booleanFlag indicates whether to remove linked resources as well...! * (needed for moving elements) */ public void removeElement(String identifier, boolean resourceFlag) { DefaultElement el = rootNode.getElementByIdentifier(identifier); if (el != null) { if (el instanceof CPItem) { // element is CPItem CPItem item = (CPItem) el; // first remove resources if (resourceFlag) { // Delete children (depth first search) removeChildElements(item, resourceFlag); // remove referenced resource CPResource res = (CPResource) rootNode.getElementByIdentifier(item.getIdentifierRef()); if (res != null && referencesCount(res) == 1) { res.removeFromManifest(); } } // then remove item item.removeFromManifest(); } else if (el instanceof CPOrganization) { // element is organization CPOrganization org = (CPOrganization) el; org.removeFromManifest(resourceFlag); } else if (el instanceof CPMetadata) { // element is <metadata> CPMetadata md = (CPMetadata) el; md.removeFromManifest(); } } else { throw new OLATRuntimeException(CPOrganizations.class, "couldn't remove element with id \"" + identifier + "\"! Element not found in manifest ", new Exception()); } } /** * Deletes all children of the element specified by the identifier * * @param identifier * @param deleteResource */ void removeChildElements(CPItem item, boolean deleteResource) { if (item != null) { for (String childIdentifier : item.getItemIdentifiers()) { removeElement(childIdentifier, deleteResource); } } } /** * Checks how many item-elements link to the given resource element. * * @param resource * @return */ protected int referencesCount(CPResource resource) { int linkCount = 0; Vector<CPItem> items = new Vector<CPItem>(); for (Iterator<CPOrganization> it = rootNode.getOrganizations().getOrganizationIterator(); it.hasNext();) { CPOrganization org = it.next(); items.addAll(org.getAllItems()); } for (CPItem item : items) { if (item.getIdentifierRef().equals(resource.getIdentifier())) linkCount++; } Vector<CPDependency> dependencies = rootNode.getResources().getAllDependencies(); for (CPDependency dependency : dependencies) { if (dependency.getIdentifierRef().equals(resource.getIdentifier())) linkCount++; } return linkCount; } public void moveElement(String nodeID, String newParentID, int position) { DefaultElement elementToMove = rootNode.getElementByIdentifier(nodeID); if (elementToMove != null) { if (elementToMove instanceof CPItem) { removeElement(nodeID, false); addElement(elementToMove, newParentID, position); } else if (elementToMove instanceof CPOrganization) { // not yet supported } else { throw new OLATRuntimeException(CPOrganizations.class, "Only <item>-elements are moveable...!", new Exception()); } } } /** * duplicates an element and inserts it after targetID * * @param sourceID * @param targetID */ public String copyElement(String sourceID, String targetID) { DefaultElement elementToCopy = rootNode.getElementByIdentifier(sourceID); if (elementToCopy == null) { throw new OLATRuntimeException(CPOrganizations.class, "element with identifier \"" + sourceID + "\" not found..!", new Exception()); } if (elementToCopy instanceof CPItem) { CPItem newItem = (CPItem) elementToCopy.clone(); cloneResourceOfItemAndSubitems(newItem); addElementAfter(newItem, targetID); return newItem.getIdentifier(); } else { // if (elementToCopy.getClass().equals(CPOrganization.class)) { // not yet supported throw new OLATRuntimeException(CPOrganizations.class, "You can only copy <item>-elements...!", new Exception()); } } /** * Clones all editable resources of the item and its subitems. * * @param item */ private void cloneResourceOfItemAndSubitems(CPItem item) { cloneResourceOfItem(item); for (CPItem child : item.getItems()) { cloneResourceOfItemAndSubitems(child); } } /** * Clones the resource of an item. If the resource is not editable, i.e. it is * not an html, Word or Excel file, there's no need to clone it and nothing * will be done. Editable resources are cloned and the single referenced file * is copied. * * @param item */ private void cloneResourceOfItem(CPItem item) { DefaultElement ref = getElementByIdentifier(item.getIdentifierRef()); if (ref != null && ref instanceof CPResource) { CPResource resource = (CPResource) ref; // Clone the resource if the linked file is editable (i.e. it is an html, // Word or Excel file) String href = resource.getFullHref(); if (href != null) { String extension = href.substring(href.lastIndexOf(".") + 1); if ("htm".equals(extension) || "html".equals(extension) || "doc".equals(extension) || "xls".equals(extension)) { CPResource clonedResource = (CPResource) resource.clone(); addElement(clonedResource); item.setIdentifierRef(clonedResource.getIdentifier()); } } } } /** * Searches for <item>-elements or <dependency>-elements which references to * the resource with id "resourceIdentifier" * * if an element is found, search is aborted and the found element is returned * * @param resourceIdentifier * @return the found element or null */ public DefaultElement findReferencesToResource(String resourceIdentifier) { // search for <item identifierref="resourceIdentifier" > for (Iterator<CPOrganization> it = rootNode.getOrganizations().getOrganizationIterator(); it.hasNext();) { CPOrganization org = it.next(); for (Iterator<CPItem> itO = org.getItems().iterator(); itO.hasNext();) { CPItem item = itO.next(); CPItem found = _findReferencesToRes(item, resourceIdentifier); if (found != null) return found; } } // search for <dependency identifierref="resourceIdentifier" > for (Iterator<CPResource> itRes = rootNode.getResources().getResourceIterator(); itRes.hasNext();) { CPResource res = itRes.next(); for (Iterator<CPDependency> itDep = res.getDependencyIterator(); itDep.hasNext();) { CPDependency dep = itDep.next(); if (dep.getIdentifierRef().equals(resourceIdentifier)) return dep; } } return null; } /** * searches recursively for <item>-elements with identifierRef "id" in the * children-collection of the item "item" * * @param item * @param id * @return */ private CPItem _findReferencesToRes(CPItem item, String id) { if (item.getIdentifierRef().equals(id)) return item; for (Iterator<CPItem> itO = item.getItems().iterator(); itO.hasNext();) { CPItem it = itO.next(); CPItem found = _findReferencesToRes(it, id); if (found != null) return found; } return null; } // *** getters *** /** * Returns the rootNode of the manifest * * @return CPManifest */ public CPManifest getRootNode() { return rootNode; } public VFSContainer getRootDir() { return rootDir; } /** * Returns the DefaultDocument of this CP * * @return the xml Document of this CP */ public DefaultDocument buildDocument() { // if (doc != null) return doc; DefaultDocument newDoc = new DefaultDocument(); rootNode.buildDocument(newDoc); return newDoc; } /** * returns the first <organization> element of this manifest Note: IMS * standard allows multiple <organization>-elements * * @return */ public CPOrganization getFirstOrganizationInManifest() { Vector<CPOrganization> orgas = rootNode.getOrganizations().getOrganizations(); // integrity check already done, there is at least one <organization> at // this moment return orgas.firstElement(); } /** * Gets the linked page for the <item> element with given id if no resource * (page) is referenced, null is returned * * @param id * @return */ public String getPageByItemID(String id) { DefaultElement ele = getElementByIdentifier(id); if (ele instanceof CPItem) { CPItem item = (CPItem) ele; if (item.getIdentifierRef() == null || item.getIdentifierRef().equals("")) { return null; } DefaultElement resElement = getElementByIdentifier(item.getIdentifierRef()); if (resElement instanceof CPResource) { CPResource res = (CPResource) resElement; return res.getFullHref(); } else { Logger log = Logger.getLogger(this.getClass().getName()); log.info("method: getPageByItemID(" + id + ") : invalid manifest.. identifierred of <item> must point to a <resource>-element"); return null; } } else { return null; } } /** * Returns the first page within the given organization returns null if no * page found (empty organization) * * @return */ public CPItem getFirstPageToDisplay() { CPOrganization orga = getFirstOrganizationInManifest(); return orga.getFirstItem(); } /** * returns the item of an <item> element (with given identifier) in the * manifest * * @param itemID the identifier of the item * @return returns the title. returns null if element is not found, or element * is not an <item> */ public String getItemTitle(String itemID) { DefaultElement ele = getElementByIdentifier(itemID); if (ele == null) { return null; } if (ele instanceof CPItem) { CPItem item = (CPItem) ele; return item.getTitle(); } else { return null; } } /** * Returns the last error of this ContentPackage (after building it.. ) * returns null, if no error occurred.. * * @return */ String getLastError() { if (errors.size() == 0) { return rootNode.getLastError(); } return errors.firstElement(); } protected void setLastError(String err) { errors.add(err); } /** * @return Returns a true if the CP was created with the OLAT CP editor or * exported from an OLAT wiki. False otherwise. */ public boolean isOLATContentPackage() { boolean isOLATCP = false; String identifier = rootNode.getIdentifier(); isOLATCP = OLAT_MANIFEST_IDENTIFIER.equals(identifier); isOLATCP |= WikiToCPExport.WIKI_MANIFEST_IDENTIFIER.equals(identifier); return isOLATCP; } }