/********************************************************************************** * $URL: $ * $Id: $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 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.site.util; import java.io.InputStream; import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.sakaiproject.authz.api.SecurityAdvisor; import org.sakaiproject.authz.cover.SecurityService; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.content.api.ContentHostingService; import org.sakaiproject.content.api.ContentCollectionEdit; import org.sakaiproject.content.api.ContentResource; import org.sakaiproject.content.api.ContentResourceEdit; import org.sakaiproject.entity.api.Reference; import org.sakaiproject.entity.cover.EntityManager; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.exception.ServerOverloadException; import org.sakaiproject.exception.TypeException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.sitemanage.api.model.*; /** * This parser is mainly to parse the questions.xml file: * @author zqian * */ public class SiteSetupQuestionFileParser { private static Log m_log = LogFactory.getLog(SiteSetupQuestionFileParser.class); private static org.sakaiproject.sitemanage.api.model.SiteSetupQuestionService questionService = (org.sakaiproject.sitemanage.api.model.SiteSetupQuestionService) ComponentManager .get(org.sakaiproject.sitemanage.api.model.SiteSetupQuestionService.class); /* Here is a template of the question.xml file: <?xml version="1.0" encoding="UTF-8" ?> <SiteSetupQuestions> <site type="project"> <header>Please answer the following to help us understand how CTools is being used for this project site.</header> <url><a href="http://www.google.com" target="_blank">More info</a></url> <question required="true" multiple_answers="false"> <q>In what capacity are you creating this site?</q> <answer>Student</answer> <answer>Faculty</answer> <answer>Staff</answer> </question> <question required="false" multiple_answers="false"> <q>The primary use for this project site will be:</q> <answer>Learning"</answer> <answer>Research"</answer> <answer>Administrative</answer> <answer>Personal</answer> <answer>Student group/organization</answer> <answer fillin_blank="true">Other</answer> </question> </site> <site type="course"> <header>Please answer the following to help us understand how CTools is being used for this course site.</header> <question required="true" multiple_answers="false"> <q>The primary use for this project site will be:</q> <answer>Student</answer> <answer>Faculty</answer> <answer>Staff</answer> </question> </site> </SiteSetupQuestions> */ private static ContentHostingService contentHostingService = (ContentHostingService) ComponentManager.get("org.sakaiproject.content.api.ContentHostingService"); protected static String m_adminSiteName = "setupQuestionsAdmin"; protected static String m_configFolder = "config"; protected static String m_configBackupFolder = "configBackup"; protected static String m_configXml = "questions.xml"; protected static SiteSetupQuestionMap m_siteSetupQuestionMap; public String getAdminSiteName() { return m_adminSiteName; } public void setAdminSiteName(String siteName) { m_adminSiteName = siteName; } /** * the reference to config folder * @return */ public static String getConfigFolderReference() { String configFolderRef = null; if(StringUtils.trimToNull(m_adminSiteName) != null && StringUtils.trimToNull(m_configFolder) != null) { configFolderRef = "/content/group/" + m_adminSiteName + "/" + m_configFolder + "/"; } return configFolderRef; } /** * the reference to config backup folder * @return */ public static String getConfigBackupFolderReference() { String configBackupFolderRef = null; if(StringUtils.trimToNull(m_adminSiteName) != null && StringUtils.trimToNull(m_configBackupFolder) != null) { configBackupFolderRef = "/content/group/" + m_adminSiteName + "/" + m_configBackupFolder + "/"; } return configBackupFolderRef; } /** * Is the configuration XML file provided and readable * @return true If the XML file is provided and readable, false otherwise */ public static boolean isConfigurationXmlAvailable() { try { String x = ""; if (m_configXml == null) { return false; } return exists(m_configXml); } catch (Exception exception) { m_log.warn("Unexpected exception: " + exception); } return false; } // /** * Does the specified configuration/hierarchy resource exist? * @param resourceName Resource name * @return true If we can access this content */ protected static boolean exists(String resourceName) { String configFolderRef = getConfigFolderReference(); if (StringUtils.trimToNull(configFolderRef) != null && StringUtils.trimToNull(resourceName)!=null) { String referenceName = configFolderRef + resourceName; Reference reference = EntityManager.newReference(referenceName); if (reference == null) return false; enableSecurityAdvisor(); ContentResource resource= null; try { resource = contentHostingService.getResource(reference.getId()); // as a remind for newly added configuration file m_log.info("exists(): find new resource " + reference.getId()); } catch (Exception ee) { // the configuration xml file are added, and get read immediately and moved to the config backup folder afterwards. Its contents are stored into database // so it is normal to find the the configuration xml missing from the original config folder // this exception is as expected, don't put it into log file } popSecurityAdvisor(); return (resource != null); } return false; } /** * Establish a security advisor to allow the "embedded" azg work to occur * with no need for additional security permissions. */ protected static void enableSecurityAdvisor() { // put in a security advisor so we can create citationAdmin site without need // of further permissions SecurityService.pushAdvisor(new SecurityAdvisor() { public SecurityAdvice isAllowed(String userId, String function, String reference) { return SecurityAdvice.ALLOWED; } }); } /** * remove recent SecurityAdvisor */ protected static void popSecurityAdvisor() { // remove recent the security advisor SecurityService.popAdvisor(); } /** * Update configuration data from an XML resource * @param configFileRef XML configuration reference (/content/...) */ public static SiteSetupQuestionMap updateConfig() { String configFolder = getConfigFolderReference(); Reference configFolderReference = EntityManager.newReference(configFolder); String configBackupFolder = getConfigBackupFolderReference(); Reference configBackupolderReference = EntityManager.newReference(configBackupFolder); Reference ref = EntityManager.newReference(configFolder + m_configXml); Reference refBackup = EntityManager.newReference(configBackupFolder + m_configXml); if (ref != null) { try { /* * Fetch configuration details from our XML resource */ enableSecurityAdvisor(); ContentResource resource = contentHostingService.getResource(ref.getId()); if (resource != null) { // step 0: set all existing questions to be non-current and remove all SiteTypeQuestions List<SiteSetupQuestion> questions = questionService.getAllSiteQuestions(); if (questions != null && !questions.isEmpty()) { for (SiteSetupQuestion question:questions) { if (question.getCurrent().equals("true")) { question.setCurrent("false"); questionService.saveSiteSetupQuestion(question); } } } questionService.removeAllSiteTypeQuestions(); // Step 1: read in questions and answers m_siteSetupQuestionMap = populateConfig(ref.getReference(), resource.streamContent()); // Step 2: make a copy of current question file // make sure the back folder exists if (!contentHostingService.isAvailable(configBackupolderReference.getId())) { try { ContentCollectionEdit fEdit = contentHostingService.addCollection(configBackupolderReference.getId(), m_configBackupFolder); contentHostingService.commitCollection(fEdit); } catch (Exception ee) { m_log.warn("SiteSetupQuestionMap.updateConfig: Problem of adding backup collection " + configBackupolderReference.getId() + ee.getMessage()); } } if (contentHostingService.isAvailable(configBackupolderReference.getId())) { try { contentHostingService.copy(ref.getId(), refBackup.getId()); } catch (Exception ee) { m_log.warn("SiteSetupQuestionMap.updateConfig: Problem of backing up question.xml file " + ee.getMessage()); } } // Step 3: remove question file try { ContentResourceEdit rEdit = contentHostingService.editResource(ref.getId()); contentHostingService.removeResource(rEdit); } catch (Exception ee) { m_log.warn("SiteSetupQuestionMap.updateConfig: Problem of removing resource " + ref.getId() + ee.getMessage()); } } // remove recent the security advisor popSecurityAdvisor(); } catch (PermissionException e) { m_log.warn("Exception: " + e + ", continuing"); } catch (IdUnusedException e) { m_log.info("configuration XML is missing (" + m_configXml + "); Citations ConfigurationService will watch for its creation"); } catch (TypeException e) { m_log.warn("Exception: " + e + ", continuing"); } catch (ServerOverloadException e) { m_log.warn("Exception: " + e + ", continuing"); } } return m_siteSetupQuestionMap; } /** * Populate cached values from a configuration XML resource. We always try * to parse the resource, regardless of any prior success or failure. * * @param configurationXml Configuration resource name (this doubles as a * unique key into the configuration cache) */ protected static SiteSetupQuestionMap populateConfig(String configurationXml, InputStream stream) { org.w3c.dom.Document document; String value; /* * Parse the XML - if that fails, give up now */ if ((document = parseXmlFromStream(stream)) == null) { return null; } SiteSetupQuestionMap m = new SiteSetupQuestionMap(); Element rootElement = document.getDocumentElement(); NodeList childList = rootElement.getChildNodes(); // root element should be "SiteSetupQuestions" if (childList == null || childList.getLength() == 0) { m_log.warn("Cannot find elements in SiteSetupQuestions"); } else { for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if (currentNode.hasAttributes()) { NamedNodeMap nNMap = currentNode.getAttributes(); String siteType = nNMap.getNamedItem("type") != null ? nNMap.getNamedItem("type").getNodeValue():null; if (siteType != null) { // add the site type into the question list SiteTypeQuestions siteTypeQuestions = questionService.newSiteTypeQuestions(); siteTypeQuestions.setSiteType(siteType); NodeList qSetList = currentNode.getChildNodes(); for (int i2 = 0; i2 < qSetList.getLength(); i2++) { Node qNode = qSetList.item(i2); switch (qNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if (qNode.getNodeName().equals("header")) { siteTypeQuestions.setInstruction(qNode.getTextContent()); } else if (qNode.getNodeName().equals("url")) { NodeList qList = qNode.getChildNodes(); for (int i3 = 0; i3 < qList.getLength(); i3++) { Node qDetailNode = qList.item(i3); switch (qDetailNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if (qDetailNode.getNodeName().equals("a")) { if (qDetailNode.hasAttributes()) { // attributes NamedNodeMap qDetailMap = qDetailNode.getAttributes(); if (qDetailMap.getNamedItem("href") != null) { siteTypeQuestions.setUrl(qDetailMap.getNamedItem("href").getNodeValue()); } else if (qDetailMap.getNamedItem("target") != null) { siteTypeQuestions.setUrlTarget(qDetailMap.getNamedItem("target").getNodeValue()); } } siteTypeQuestions.setUrlLabel(qDetailNode.getTextContent()); } } } } else if (qNode.getNodeName().equals("question")) { SiteSetupQuestion q = questionService.newSiteSetupQuestion(); if (qNode.hasAttributes()) { // attributes NamedNodeMap qMap = qNode.getAttributes(); if (qMap.getNamedItem("required") != null) { q.setRequired(Boolean.valueOf(qMap.getNamedItem("required").getNodeValue())); } else { q.setRequired(false); } if (qMap.getNamedItem("multiple_answers") != null) { q.setIsMultipleAnswers(Boolean.valueOf(qMap.getNamedItem("multiple_answers").getNodeValue())); } else { q.setIsMultipleAnswers(false); } NodeList qList = qNode.getChildNodes(); for (int i3 = 0; i3 < qList.getLength(); i3++) { Node qDetailNode = qList.item(i3); switch (qDetailNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if (qDetailNode.getNodeName().equals("q")) { q.setQuestion(qDetailNode.getTextContent()); } else if (qDetailNode.getNodeName().equals("answer")) { SiteSetupQuestionAnswer answer = questionService.newSiteSetupQuestionAnswer(); if (qDetailNode.hasAttributes()) { // attributes NamedNodeMap qDetailMap = qDetailNode.getAttributes(); if (qDetailMap.getNamedItem("fillin_blank") != null) { answer.setIsFillInBlank(Boolean.valueOf(qDetailMap.getNamedItem("fillin_blank").getNodeValue())); } else { answer.setIsFillInBlank(false); } } answer.setAnswer(qDetailNode.getTextContent()); // save answer questionService.saveSiteSetupQuestionAnswer(answer); q.addAnswer(answer); } break; } } } // mark this question as currently used q.setCurrent("true"); // save question questionService.saveSiteSetupQuestion(q); siteTypeQuestions.addQuestion(q); } break; } } // save siteTypeQuestions questionService.saveSiteTypeQuestions(siteTypeQuestions); } } break; } } } // what to do? return m; } /** * Lookup and save one dynamic configuration parameter * @param Configuration XML * @param parameterMap Parameter name=value pairs * @param name Parameter name */ protected void saveParameter(org.w3c.dom.Document document, Map parameterMap, String name) { String value; if ((value = getText(document, name)) != null) { parameterMap.put(name, value); } } /* * XML helpers */ /** * Parse an XML resource * @param filename The filename (or URI) to parse * @return DOM Document (null if parse fails) */ protected static Document parseXmlFromStream(InputStream stream) { try { DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); builderFactory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true); builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder(); if (documentBuilder != null) { return documentBuilder.parse(stream); } } catch (Exception exception) { m_log.warn("XML parse on \"" + stream + "\" failed: " + exception); } return null; } // xml helper /** * Get a DOM Document builder. * @return The DocumentBuilder * @throws DomException */ protected static DocumentBuilder getXmlDocumentBuilder() { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(false); return factory.newDocumentBuilder(); } catch (Exception exception) { m_log.warn("Failed to get XML DocumentBuilder: " + exception); } return null; } /** * "Normalize" XML text node content to create a simple string * @param original Original text * @param update Text to add to the original string (a space separates the two) * @return Concatenated contents (trimmed) */ protected String normalizeText(String original, String update) { StringBuilder result; if (original == null) { return (update == null) ? "" : update.trim(); } result = new StringBuilder(original.trim()); result.append(' '); result.append(update.trim()); return result.toString(); } /** * Get the text associated with this element * @param root The document containing the text element * @return Text (trimmed of leading/trailing whitespace, null if none) */ protected String getText(Document root, String elementName) { return getText(root.getDocumentElement(), elementName); } /** * Get the text associated with this element * @param root The root node of the text element * @return Text (trimmed of leading/trailing whitespace, null if none) */ protected String getText(Element root, String elementName) { NodeList nodeList; Node parent; String text; nodeList = root.getElementsByTagName(elementName); if (nodeList.getLength() == 0) { return null; } text = null; parent = (Element) nodeList.item(0); for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { switch (child.getNodeType()) { case Node.TEXT_NODE: text = normalizeText(text, child.getNodeValue()); break; default: break; } } return text == null ? text : text.trim(); } }