001: /* Copyright 2005 The JA-SIG Collaborative. All rights reserved.
002: * See license distributed with this file and
003: * available online at http://www.uportal.org/license.html
004: */
005: package org.jasig.portal.channels.adminnav.provider;
006:
007: import java.util.Iterator;
008: import java.util.LinkedList;
009: import java.util.Locale;
010: import java.util.Map;
011: import java.util.MissingResourceException;
012:
013: import org.apache.commons.logging.Log;
014: import org.apache.commons.logging.LogFactory;
015: import org.jasig.portal.AuthorizationException;
016: import org.jasig.portal.ChannelCacheKey;
017: import org.jasig.portal.ChannelDefinition;
018: import org.jasig.portal.ChannelRegistryStoreFactory;
019: import org.jasig.portal.ChannelRuntimeData;
020: import org.jasig.portal.ChannelStaticData;
021: import org.jasig.portal.Constants;
022: import org.jasig.portal.IChannelRegistryStore;
023: import org.jasig.portal.PortalException;
024: import org.jasig.portal.PortalSessionManager;
025: import org.jasig.portal.UPFileSpec;
026: import org.jasig.portal.UserInstance;
027: import org.jasig.portal.channels.BaseChannel;
028: import org.jasig.portal.channels.adminnav.XMLLinksFileLoader;
029: import org.jasig.portal.channels.adminnav.ILabelResolver;
030: import org.jasig.portal.channels.adminnav.INavigationModel;
031: import org.jasig.portal.channels.adminnav.ResourceBundleResolver;
032: import org.jasig.portal.security.IAuthorizationPrincipal;
033: import org.jasig.portal.utils.DocumentFactory;
034: import org.jasig.portal.utils.XSLT;
035: import org.w3c.dom.Document;
036: import org.w3c.dom.Element;
037: import org.xml.sax.ContentHandler;
038:
039: /**
040: * This channel provides a flat list of urls (links) to other channels using the
041: * channel functional names. When selected these links cause that channel to be
042: * rendered in focused mode.
043: *
044: * This implementation supports both static and dynamic registration. Dynamic
045: * registration takes place any time via calls to addLink(). The set of static
046: * links is defined in /properties/adminNav.xml. Their locale specific text is
047: * loaded from /properties/adminNav.properties or a suitable derivative for a
048: * specific locale.
049: *
050: * CHeader channel presents the "Channel Admin" link which brings this channel
051: * into focused mode when selected. That link will only render if this channel
052: * has registered links that point to channels for which the current user has
053: * authorization. Similarly, when this channel renders, it only presents those
054: * links to channels for which the current user has authorization.
055: *
056: * Localization of link labels is supported through instances of ILabelResolver
057: * passed in at link registration time.
058: *
059: * @author Keith Stacks, kstacks@sungardsct.com
060: * @author Mark Boyd, mboyd@sungardsct.com
061: */
062: public class ListModel extends BaseChannel implements INavigationModel {
063:
064: // Stylesheet file should be co-located with this java file.
065: private static final String XSL_LOCATION = "navigation.xsl";
066:
067: // complete list of nav links available
068: private static LinkedList cLinks = new LinkedList();
069:
070: // Rendering Document
071: private Document mDoc = null;
072:
073: // property used to acquire localized header label
074: private static final String HEADER_PROPERTY = "admin.nav.header";
075:
076: // config file location
077: private static final String CONFIG_FILE = "/properties/adminNav.xml";
078:
079: // resource bundle base for config file labels
080: private static final String BUNDLE_BASE = "/properties/adminNav";
081:
082: // instance of our label resolver
083: private static final ILabelResolver resolver = new ResourceBundleResolver(
084: BUNDLE_BASE);
085:
086: // Used for informational, error, and debug logging
087: private static Log LOG = LogFactory.getLog(ListModel.class);
088:
089: // The cache key used to indicate whether the cached rendering output is
090: // still valid or should be re-rendered.
091: private ChannelCacheKey cacheKey = new ChannelCacheKey();
092:
093: public ListModel() {
094: XMLLinksFileLoader loader = new XMLLinksFileLoader(CONFIG_FILE,
095: this , resolver);
096: }
097:
098: /************* Utility class **************/
099:
100: private static class Link {
101: String labelId = null;
102: String url = null;
103: ILabelResolver resolver = null;
104: int publishIdOfTargetChannel = -1;
105:
106: public Link(String labelId, ILabelResolver resolver,
107: String url, int pubIdOfTargetChannel) {
108: this .labelId = labelId;
109: this .resolver = resolver;
110: this .url = url;
111: this .publishIdOfTargetChannel = pubIdOfTargetChannel;
112: }
113: }
114:
115: /**
116: * Returns true if the user represented by the passed-in authorization
117: * principal returns has access to any of the channels pointed to by
118: * registered links. This is used by CHeader to determine if the "Channel
119: * Admin" link should be rendered.
120: */
121: public boolean canAccess(IAuthorizationPrincipal ap) {
122: for (Iterator iter = cLinks.iterator(); iter.hasNext();) {
123: Link link = (Link) iter.next();
124: try {
125: if (ap.canSubscribe(link.publishIdOfTargetChannel))
126: return true;
127: } catch (AuthorizationException e) {
128: if (LOG.isDebugEnabled())
129: LOG
130: .debug(
131: "Unable to determine if principal "
132: + ap.getPrincipalString()
133: + " can subscribe to channel with publish ID "
134: + link.publishIdOfTargetChannel
135: + ", and url " + link.url,
136: e);
137: }
138: }
139: return false;
140: }
141:
142: /**
143: * Add a link to the channel indicated by the passed in functional name to
144: * the list of links located in the admin navigation list. The label will be
145: * the text shown in the UI for the link. The name/value pairs passed in via
146: * the parameters argument will be appended as query parameters.
147: *
148: * @param fname
149: * the functional name of a published channel. This must not be
150: * null and must correspond to the functional name of an already
151: * published channel.
152: * @param labelId
153: * the test that should show in the UI for this link. This must
154: * not be null.
155: * @param parameters
156: * additional query parameter name/value pairs to be appended to
157: * the URL if needed for the link. This value can be null if no
158: * additional parameters are needed.
159: */
160: public void addLink(String fname, String labelId,
161: ILabelResolver resolver, Map parameters) {
162: try {
163: // first perform some edit checks
164: if (fname == null || fname.equals(""))
165: throw new Exception(
166: "'Functional Name' must be specified.");
167:
168: if (labelId == null || labelId.equals("")) {
169: labelId = "unspecified";
170: throw new Exception("'Label' must be specified.");
171: }
172: // now get pub ID of target channel
173: IChannelRegistryStore crs = ChannelRegistryStoreFactory
174: .getChannelRegistryStoreImpl();
175: ChannelDefinition chanDef = crs.getChannelDefinition(fname);
176: int pubId = chanDef.getId();
177:
178: // next build the URL for the link
179: String url = UPFileSpec.buildUPFile(
180: PortalSessionManager.IDEMPOTENT_URL_TAG,
181: UPFileSpec.RENDER_METHOD,
182: UserInstance.USER_LAYOUT_ROOT_NODE, null, null);
183:
184: url = url + "?" + Constants.FNAME_PARAM + "=" + fname;
185:
186: if (parameters != null) {
187: for (Iterator iter = parameters.keySet().iterator(); iter
188: .hasNext();) {
189: String name = (String) iter.next();
190: String value = (String) parameters.get(name);
191: url += "&" + name + "=" + value;
192: }
193: }
194: cLinks.add(new Link(labelId, resolver, url, pubId));
195:
196: // force refresh of channel UI.
197: cacheKey.setKeyValidity(new Locale("", ""));
198: } catch (Exception e) {
199: LOG.error("Unable to add link '" + labelId
200: + "' to administration navigation list.", e);
201: }
202: }
203:
204: /**
205: * Return the reused cache key. Only the internal validity is used and
206: * handed back via isCacheValid().
207: */
208: public ChannelCacheKey generateKey() {
209: return cacheKey;
210: }
211:
212: /**
213: * The validity object used in our cache key is the locale used to generate
214: * the XML for the channel. So cache refresh will only take place when the
215: * user changes their locale.
216: */
217: public boolean isCacheValid(Object validity) {
218: if (validity != cacheKey.getKeyValidity())
219: return false;
220: return true;
221: }
222:
223: public void setStaticData(ChannelStaticData sd)
224: throws PortalException {
225: super .setStaticData(sd);
226: cacheKey.setKeyScope(ChannelCacheKey.INSTANCE_KEY_SCOPE);
227: cacheKey.setKey(this .getClass().getName()
228: + sd.getChannelSubscribeId());
229: cacheKey.setKeyValidity(new Locale("", ""));
230: }
231:
232: /**
233: * Checks to see if the rendering document needs to be updated for the
234: * user's locale.
235: */
236: public void setRuntimeData(ChannelRuntimeData rd)
237: throws PortalException {
238: super .setRuntimeData(rd);
239:
240: // see if the user has changed their locale since the last time that
241: // the model was generated.
242: Locale[] locales = rd.getLocales();
243: Locale currentLocale = null;
244:
245: if (locales == null)
246: currentLocale = Locale.US;
247: else if (locales.length == 0)
248: currentLocale = Locale.US;
249: else if (locales[0] == null)
250: currentLocale = Locale.US;
251: else
252: currentLocale = locales[0];
253:
254: Locale lastLocale = (Locale) cacheKey.getKeyValidity();
255:
256: if (mDoc == null
257: || !lastLocale.toString().equals(
258: currentLocale.toString())) {
259: generateXML(currentLocale);
260: cacheKey.setKeyValidity(currentLocale);
261: }
262: }
263:
264: /**
265: * Render the links.
266: *
267: * @param out stream that handles output
268: */
269: public void renderXML(ContentHandler out) throws PortalException {
270: XSLT xslt = new XSLT(this );
271: xslt.setXML(mDoc);
272: xslt.setXSL(XSL_LOCATION);//optionsLabel
273: xslt.setTarget(out);
274: xslt.transform();
275: }
276:
277: /**
278: * Generates the XML DOM used in rendering the UI.
279: **/
280: private void generateXML(Locale locale) {
281: Document doc = DocumentFactory.getNewDocument();
282: Element root = doc.createElement("adminurls");
283: doc.appendChild(root);
284: Element heading = doc.createElement("heading");
285: root.appendChild(heading);
286: heading.appendChild(doc.createTextNode(resolveLabel(resolver,
287: HEADER_PROPERTY, locale)));
288:
289: IAuthorizationPrincipal ap = staticData
290: .getAuthorizationPrincipal();
291:
292: for (Iterator iter = cLinks.iterator(); iter.hasNext();) {
293: // determine if user has permission for rendering
294: Link link = (Link) iter.next();
295:
296: try {
297: if (ap.canSubscribe(link.publishIdOfTargetChannel)) {
298: if (LOG.isDebugEnabled())
299: LOG.debug("User can render channel '"
300: + link.publishIdOfTargetChannel
301: + "' with url '" + link.url + "'");
302:
303: Element adminURLEl = doc.createElement("adminurl");
304: adminURLEl.setAttribute("desc", resolveLabel(
305: link.resolver, link.labelId, locale));
306: adminURLEl
307: .appendChild(doc.createTextNode(link.url));
308: root.appendChild(adminURLEl);
309: }
310: } catch (AuthorizationException e) {
311: if (LOG.isDebugEnabled())
312: LOG.debug("Unable to add link for channel '"
313: + link.publishIdOfTargetChannel
314: + "' with url '" + link.url + "'");
315: }
316: }
317:
318: mDoc = doc;
319: }
320:
321: /**
322: * Handles resolving labels and providing default if a null value is
323: * returned from a resolver or the resolver tosses a missing resource
324: * exception typical from underlying resource bundle implementations.
325: *
326: * @param resolver2
327: * @param labelId
328: * @param locale
329: * @return
330: */
331: private String resolveLabel(ILabelResolver resolver,
332: String labelId, Locale locale) {
333: String label = null;
334:
335: try {
336: label = resolver.getLabel(labelId, locale);
337: } catch (MissingResourceException mre) {
338: // ignore since we handle null below.
339: }
340: if (label == null) {
341: StringBuffer sb = new StringBuffer().append("???").append(
342: resolver.getClass().getName());
343:
344: String resExtForm = resolver.getExternalForm();
345:
346: if (resExtForm != null && !resExtForm.equals(""))
347: sb.append('{').append(resExtForm).append('}');
348:
349: sb.append("[").append(labelId).append("]???");
350: label = sb.toString();
351: }
352: return label;
353: }
354: }
|