/** * Copyright © 2001 The JA-SIG Collaborative. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the JA-SIG Collaborative * (http://www.jasig.org/)." * * THIS SOFTWARE IS PROVIDED BY THE JA-SIG COLLABORATIVE "AS IS" AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE JA-SIG COLLABORATIVE OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * */ package org.jasig.portal.channels; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.regex.Pattern; import java.util.regex.Matcher; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.NotContextException; import org.jasig.portal.ChannelCacheKey; import org.jasig.portal.ChannelRuntimeData; import org.jasig.portal.ChannelRuntimeProperties; import org.jasig.portal.ChannelStaticData; import org.jasig.portal.ICCRegistry; import org.jasig.portal.GeneralRenderingException; import org.jasig.portal.IMultithreadedCacheable; import org.jasig.portal.IMultithreadedChannel; import org.jasig.portal.PortalEvent; import org.jasig.portal.PortalException; import org.jasig.portal.ResourceMissingException; import org.jasig.portal.i18n.LocaleManager; import org.jasig.portal.security.LocalConnectionContext; import org.jasig.portal.services.PersonDirectory; import org.jasig.portal.channels.iccdemo.ViewerURL; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.portal.utils.DTDResolver; import org.jasig.portal.utils.ResourceLoader; import org.jasig.portal.utils.XSLT; import org.w3c.dom.Document; import org.xml.sax.ContentHandler; import org.jasig.portal.utils.DocumentFactory; import org.w3c.dom.Element; import org.apache.xml.serialize.XMLSerializer; import java.io.PrintWriter; import java.io.FileOutputStream; /** *

A channel which transforms XML for rendering in the portal.

* *

* Static channel parameters to be supplied: * 1) "xmlUri" - a URI representing the source XML document * 2) "upc_localConnContext" - The class name of the ILocalConnectionContext * implementation. Use when local data needs to be sent with the request for the URL. *

*

* This channel is intended to be used to display course-specific information. It can be * edited by the course instructor, who publishes the channel, and includes 10 editable * links to be viewed through interchannel-communication similiar to the ICCVIewer channel, * except the URLs are contained in the course-specific XML file rather than the JNDI context. * @author rtwigg@uccs.edu, adapted from CGenericXSLT.java and CURLSelector.java * @version $Revision: 2.4.1 $ */ public class CCourse extends BaseChannel implements IMultithreadedChannel, IMultithreadedCacheable { private static final Log log = LogFactory.getLog(CCourse.class); Map stateTable; static final String systemCacheId="org.jasig.portal.channels.CCourse"; // to prepend to the system-wide cache key private static final String sslLocation = "CCourse/CCourse.ssl"; private static final String viewerFname="/portal/iccdemo/viewer"; // state class private class CState { private String xmlUri; private String instructorP; private String iName; private String iWebsite; private Map params; private long cacheTimeout; private ChannelRuntimeData runtimeData; private LocalConnectionContext localConnContext; private String action; private boolean refresh; public CState() { xmlUri = instructorP = iName = action = null; params = new HashMap(); runtimeData = null; localConnContext = null; refresh=false; } public String toString() { StringBuffer str = new StringBuffer(); str.append("xmlUri = "+xmlUri+"\n"); if (params != null) { str.append("params = "+params.toString()+"\n"); } return str.toString(); } } public CCourse() { stateTable = Collections.synchronizedMap(new HashMap()); } public void setStaticData (ChannelStaticData sd, String uid) throws ResourceMissingException, PortalException { super.setStaticData(sd); CState state = new CState(); state.xmlUri = sd.getParameter("xmlUri"); state.instructorP = sd.getParameter("instructor"); state.action="view"; state.refresh=false; registerListeners(); String cacheTimeout = sd.getParameter("cacheTimeout"); if (cacheTimeout != null) { state.cacheTimeout = Integer.parseInt(cacheTimeout); } String connContext = sd.getParameter ("upc_localConnContext"); if (connContext != null) { try { state.localConnContext = (LocalConnectionContext) Class.forName(connContext).newInstance(); state.localConnContext.init(sd); } catch (Exception e) { log.error( "CCourse: Cannot initialize ILocalConnectionContext: " + e); } } // Get instructor display name from person attributes Hashtable attribs = PersonDirectory.instance().getUserDirectoryInformation(state.instructorP); String iName = (String)attribs.get("displayName"); if (iName != null) { state.iName = iName; } String iWebsite = (String)attribs.get("labeledURI"); if (iWebsite != null) { state.iWebsite = iWebsite; } stateTable.put(uid,state); } public void setRuntimeData (ChannelRuntimeData rd, String uid) throws PortalException { CState state = (CState)stateTable.get(uid); if (state == null) { log.error("CCourse:setRuntimeData(): Attempting to access a non-established channel! setStaticData() has never been called on the =uid=\""+uid+"\""); } else { state.runtimeData=rd; String form_action = rd.getParameter("uPCC_action"); if (form_action != null) { if (form_action.equals("view")) { String url = rd.getParameter("url"); if (url != null) { setViewerURL(url); } } else if (form_action.equals("save")) { state.action = "view"; state.refresh=true; String xmlPath = null; try { if (state.localConnContext != null) { xmlPath = ResourceLoader.getResourceAsFileString(this.getClass(), state.localConnContext.getDescriptor(state.xmlUri, rd)); } else { xmlPath = ResourceLoader.getResourceAsFileString(this.getClass(), state.xmlUri); } } catch (Exception e) { throw new ResourceMissingException (state.xmlUri, "", e.getMessage()); } try { Document emptyDoc = DocumentFactory.getNewDocument(); Element courseE = emptyDoc.createElement("course"); Element instructorE = emptyDoc.createElement("instructor"); String instructor = rd.getParameter("instructor"); if (instructor != null) { instructorE.appendChild(emptyDoc.createTextNode(instructor)); // If no intructor entered, use instructor param to get display name from persondir } else { if (state.iName != null) { instructorE.appendChild(emptyDoc.createTextNode(state.iName)); } else { instructorE.appendChild(emptyDoc.createTextNode("")); } } courseE.appendChild(instructorE); Element officeHoursE = emptyDoc.createElement("officeHours"); String officeHours = rd.getParameter("officeHours"); if (officeHours != null) { officeHoursE.appendChild(emptyDoc.createTextNode(officeHours)); } courseE.appendChild(officeHoursE); Element lectureHoursE = emptyDoc.createElement("lectureHours"); String lectureHours = rd.getParameter("lectureHours"); if (lectureHours != null) { lectureHoursE.appendChild(emptyDoc.createTextNode(lectureHours)); } courseE.appendChild(lectureHoursE); Element lectureLocE = emptyDoc.createElement("lectureLoc"); String lectureLoc = rd.getParameter("lectureLoc"); if (lectureLoc != null) { lectureLocE.appendChild(emptyDoc.createTextNode(lectureLoc)); } courseE.appendChild(lectureLocE); Element memoE = emptyDoc.createElement("memo"); String memo = rd.getParameter("memo"); if (memo != null) { memoE.appendChild(emptyDoc.createTextNode(memo)); } courseE.appendChild(memoE); Element urlListE = emptyDoc.createElement("urlList"); for (int i=1; i<11; i++) { Element urlE = emptyDoc.createElement("url"); String enumDName = "dName" + i; String enumFName = "fName" + i; String dName = rd.getParameter(enumDName); // enumerated URL display name String fName = rd.getParameter(enumFName); // enumerated URL functional name if (fName != null) { // Look for "\\" in url and ad default "http:\\" to url if absent Pattern pattern = Pattern.compile("//"); Matcher matcher = pattern.matcher(fName); if (!(matcher.find())) { fName = "http://" + fName; } urlE.setAttribute("fName", fName + ""); } if (dName != null) { urlE.setAttribute("dName", dName + ""); } urlListE.appendChild(urlE); } courseE.appendChild(urlListE); emptyDoc.appendChild(courseE); PrintWriter out = new PrintWriter(new FileOutputStream(xmlPath)); XMLSerializer serializer = new XMLSerializer(out,null); serializer.serialize(emptyDoc); out.flush(); out.close(); } catch (Exception e) { log.error( "CCourse::setRuntimeData(): Unable to save xml document " + e.getMessage()); } } else if (form_action.equals("cancel")) { state.action = "view"; } } // end if(form_action != null) // grab the parameters and stuff them all into the state object Enumeration enum = rd.getParameterNames(); while (enum.hasMoreElements()) { String n = (String)enum.nextElement(); if (rd.getParameter(n) != null) { state.params.put(n,rd.getParameter(n)); } } } // end else(state==null) } public void receiveEvent (PortalEvent ev, String uid) { CState state=(CState)stateTable.get(uid); if (ev.getEventNumber() == PortalEvent.SESSION_DONE) { // clean up stateTable.remove(uid); } if (ev.getEventNumber() == PortalEvent.EDIT_BUTTON_EVENT) { state.action="edit"; state.refresh=true; } } public ChannelRuntimeProperties getRuntimeProperties (String uid) { ChannelRuntimeProperties rp=new ChannelRuntimeProperties(); CState state=(CState)stateTable.get(uid); // determine if such channel is registered if (stateTable.get(uid) == null) { rp.setWillRender(false); log.error("CCourse::getRuntimeProperties() : attempting to access a non-established channel! setStaticData() has never been called on the uid=\""+uid+"\""); } return rp; } public void renderXML(ContentHandler out,String uid) throws PortalException { CState state=(CState)stateTable.get(uid); state.refresh=false; if (state == null) log.error("CCourse::renderXML(): Attempting to access a non-established channel! setStaticData() has never been called on the uid=\""+uid+"\""); else { log.debug("CCourse::renderXML(): State = " + state.toString() ); String xml; Document xmlDoc, xmlClone; InputStream inputStream = null; try { DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); docBuilderFactory.setNamespaceAware(true); DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); DTDResolver dtdResolver = new DTDResolver(); docBuilder.setEntityResolver(dtdResolver); URL url; try { if (state.localConnContext != null) { url = ResourceLoader.getResourceAsURL(this.getClass(), state.localConnContext.getDescriptor(state.xmlUri, state.runtimeData)); } else { url = ResourceLoader.getResourceAsURL(this.getClass(), state.xmlUri); } URLConnection urlConnect = url.openConnection(); if (state.localConnContext != null) { try { state.localConnContext.sendLocalData(urlConnect, state.runtimeData); } catch (Exception e) { log.error( "CCourse:: renderXML(): Unable to send data through " + state.runtimeData.getParameter("upc_localConnContext") + ": " + e.getMessage()); } } inputStream = urlConnect.getInputStream(); xmlDoc = docBuilder.parse(inputStream); // Clone the original document xmlClone = (Document)xmlDoc.cloneNode(true); } catch (IOException ioe) { throw new ResourceMissingException (state.xmlUri, "", ioe.getMessage()); } } catch (Exception e) { throw new GeneralRenderingException("Problem parsing " + state.xmlUri + ": " + e); } finally { try { if (inputStream != null) inputStream.close(); } catch (IOException ioe) { throw new PortalException("CCourse::renderXML():: could not close InputStream"); } } state.runtimeData.put("baseActionURL", state.runtimeData.getBaseActionURL()); state.runtimeData.put("isRenderingAsRoot", String.valueOf(state.runtimeData.isRenderingAsRoot())); // For edit event if (state.action != null) { state.runtimeData.put("action", state.action); } // Default instructor name if (state.iName != null) { state.runtimeData.put("iName", state.iName); } // Default instructor website if (state.iWebsite != null) { state.runtimeData.put("iWebsite", state.iWebsite); } // OK, pass everything we got cached in params... if (state.params != null) { Iterator it = state.params.keySet().iterator(); while (it.hasNext()) { String n = (String)it.next(); if (state.params.get((Object)n) != null) { state.runtimeData.put(n,state.params.get((Object)n)); } } } XSLT xslt = XSLT.getTransformer(this); xslt.setXML(xmlClone); xslt.setXSL(sslLocation, state.runtimeData.getBrowserInfo()); xslt.setTarget(out); xslt.setStylesheetParameters(state.runtimeData); xslt.transform(); } } /** * A utility method to register viewer channel as * listeners of the current channel */ private void registerListeners() { ICCRegistry r=staticData.getICCRegistry(); String viewerId=getChannelId(viewerFname); if(viewerId!=null) { // add a listener channel r.addListenerChannel(viewerId); } } /** * A utility method that communicates the new url to the viewer channel * * @param url a String value */ private void setViewerURL(String url) { // find viewer's id String viewerId=getChannelId(viewerFname); if(viewerId!=null) { ViewerURL v=(ViewerURL) getBoundObject(viewerId); if(v!=null) { v.setNewURL(url); } } } /** * A utility method for obtaining a channelSubscribeId given a channel fname * * @param fname a String value * @return channel's subscribe id, or null if no channel with given fname was found */ private String getChannelId(String fname) { String id=null; Context globalIDContext = null; try { // Get the context that holds the global IDs for this user globalIDContext = (Context)staticData.getJNDIContext().lookup("/channel-ids"); } catch (NotContextException nce) { log.error( "CCourse::getChannelId(): Could not find subcontext /channel-ids in JNDI"); } catch (NamingException e) { log.error("Lookup /channel-ids failed", e); } try { id=(String)globalIDContext.lookup(fname); } catch (NotContextException nce) { log.error( "CCourse::getChannelId(): Could not find channel ID for fname="+fname); } catch (NamingException e) { log.error("Lookup " + fname + " failed", e); } return id; } /** * A utility method to determine an object bound to the "chan-obj" branch * for a particular channel id. * * @param channelSubscribeId a String value of the channel who's object we're looking for * @return an Object value bound to that jndi location (or null if channel didn't bind anything) */ private Object getBoundObject(String channelSubscribeId) { Object o=null; Context globalObjContext = null; try { globalObjContext = (Context)staticData.getJNDIContext().lookup("/channel-obj"); } catch (NotContextException nce) { log.error( "CCourse::getChannelId(): Could not find subcontext /channel-obj in JNDI"); } catch (NamingException e) { log.error("Lookup /channel-obj failed", e); } try { o=globalObjContext.lookup(channelSubscribeId); } catch (NotContextException nce) { log.error( "CCourse::getChannelId(): Could not find channel bound object for channel id="+channelSubscribeId); } catch (NamingException e) { log.error("Lookup " + channelSubscribeId + " failed", e); } return o; } public ChannelCacheKey generateKey(String uid) { CState state = (CState)stateTable.get(uid); if (state == null) { log.error("CDeptMsg:generateKey() : attempting to access a non-established channel! setStaticData() has never been called on the uid=\""+uid+"\""); return null; } else { if (state.refresh) { return null; } else { ChannelCacheKey k = new ChannelCacheKey(); k.setKey(this.getKey(state)+","+uid); k.setKeyScope(ChannelCacheKey.SYSTEM_KEY_SCOPE); k.setKeyValidity(new Long(System.currentTimeMillis())); return k; } } } public boolean isCacheValid(Object validity,String uid) { if (!(validity instanceof Long)) return false; CState state = (CState)stateTable.get(uid); if (state == null) { log.error("CCourse::isCacheValid() : attempting to access a non-established channel! setStaticData() has never been called on the uid=\""+uid+"\""); return false; } else return (System.currentTimeMillis() - ((Long)validity).longValue() < state.cacheTimeout*1000); } private String getKey(CState state) { // Maybe not the best way to generate a key, but it seems to work. // If you know a better way, please change it! StringBuffer sbKey = new StringBuffer(1024); sbKey.append(systemCacheId).append(": "); sbKey.append("xmluri:").append(state.xmlUri).append(", "); String xslUriForKey = null; try { String sslUri = ResourceLoader.getResourceAsURLString(CCourse.class, sslLocation); xslUriForKey = XSLT.getStylesheetURI(sslUri, state.runtimeData.getBrowserInfo()); } catch (Exception e) { xslUriForKey = "Not attainable: " + e; } sbKey.append("locales:").append(LocaleManager.stringValueOf(state.runtimeData.getLocales())); sbKey.append("xslUri:").append(xslUriForKey).append(", "); sbKey.append("cacheTimeout:").append(state.cacheTimeout).append(", "); sbKey.append("isRenderingAsRoot:").append(state.runtimeData.isRenderingAsRoot()).append(", "); // If a local connection context is configured, include its descriptor in the key if (state.localConnContext != null) sbKey.append("descriptor:").append(state.localConnContext.getDescriptor(state.xmlUri, state.runtimeData)).append(", "); sbKey.append("params:").append(state.params.toString()); return sbKey.toString(); } }