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:
006: package org.jasig.portal.layout.dlm;
007:
008: import java.util.ArrayList;
009: import java.util.Arrays;
010: import java.util.Comparator;
011: import java.util.Iterator;
012: import java.util.List;
013:
014: import org.apache.commons.logging.Log;
015: import org.apache.commons.logging.LogFactory;
016: import org.jasig.portal.PortalException;
017: import org.jasig.portal.layout.IUserLayoutStore;
018: import org.jasig.portal.layout.UserLayoutStoreFactory;
019: import org.jasig.portal.security.IPerson;
020: import org.w3c.dom.Document;
021: import org.w3c.dom.Element;
022: import org.w3c.dom.Node;
023:
024: /**
025: * Applies and updates position specifiers for child nodes in the
026: * composite layout.
027: *
028: * @version $Revision: 36294 $ $Date: 2005-11-10 12:36:31 -0700 (Thu, 10 Nov 2005) $
029: * @since uPortal 2.5
030: */
031: public class PositionManager {
032: public static final String RCS_ID = "@(#) $Header$";
033: private static Log LOG = LogFactory.getLog(PositionManager.class);
034:
035: private static RDBMDistributedLayoutStore dls = null;
036:
037: /**
038: * Hands back the single instance of RDBMDistributedLayoutStore. There is
039: * already a method
040: * for aquiring a single instance of the configured layout store so we
041: * delegate over there so that all references refer to the same instance.
042: * This method is solely for convenience so that we don't have to keep
043: * calling UserLayoutStoreFactory and casting the resulting class.
044: */
045: private static RDBMDistributedLayoutStore getDLS() {
046: if (dls == null) {
047: IUserLayoutStore uls = null;
048: uls = UserLayoutStoreFactory.getUserLayoutStoreImpl();
049: dls = (RDBMDistributedLayoutStore) uls;
050: }
051: return dls;
052: }
053:
054: /**
055: This method and ones that it delegates to have the responsibility of
056: organizing the child nodes of the passed in composite view parent node
057: according to the order specified in the passed in position set and
058: return via the passed in result set whether the personal layout
059: fragment (one portion of which is the position set) or the incoporated
060: layouts fragment (one portion of which is the compViewParent) were
061: changed.
062:
063: This may also include pulling nodes in from other parents under certain
064: circumstances. For example, if allowed a user can move nodes that are
065: not part of their personal layout fragment or PLF; the UI elements that
066: they own. These node do not exist in their layout in the database but
067: instead are merged in with their owned elements at log in and other
068: times. So to move them during subsequent merges a position set can
069: contain a position directive indicating the id of the node to be moved
070: into a specific position in the sibling list and that well may refer to
071: a node not in the sibling list to begin with. If the node no longer
072: exists in the composite view then that position directive can safely be
073: discarded.
074:
075: Positioning is meant to preserve as much as possible the user's
076: specified ordering of user interface elements but always respecting
077: movement restrictions placed on those elements that are incorporated by
078: their owners. So the following rules apply from most important to least.
079:
080: 1) nodes with moveAllowed="false" prevent nodes of lower precedence from
081: being to their left or higher with left or higher defined as having a
082: lower index in the sibling list. (applyLowerPrecRestriction)
083:
084: 2) nodes with moveAllowed="false" prevent nodes of equal precedence from
085: moving from one side of this node to the other from their position as
086: found in the compViewParent initially and prevents nodes with the same
087: precedence from moving from other parents into this parent prior to the
088: restricted node. Prior to implies a lower sibling index.
089: (applyHoppingRestriction)
090:
091: 3) nodes with moveAllowed="false" prevent nodes of equal precedence
092: lower in the sibling list from being reparented. (ie: moving to another
093: parent) However, they can be deleted. (applyReparentingCheck)
094:
095: 4) nodes should be ordered as much as possible in the order specified by
096: the user but in view of the above conditions. So if a user has moved
097: nodes thus specifying some order and the owner of some node in that set
098: then locks one of those nodes some of those nodes will have to move
099: back to their orinial positions to conform with the rules above but for
100: the remaining nodes they should be found in the same relative order
101: specified by the user. (getOrder)
102:
103: 5) nodes not included in the order specified by the user (ie: nodes
104: added since the user last ordered them) should maintain their relative
105: order as much as possible and be appended to the end of the sibling
106: list after all others rules have been applied. (getOrder)
107:
108: Each of these rules is applied by a call to a method 5 being first and
109: 1 being last so that 1 has the highest precedence and last say. Once
110: the final ordering is specified then it is applied to the children of
111: the compViewParent and returned.
112: */
113: static void applyPositions(Element compViewParent,
114: Element positionSet, IntegrationResult result)
115: throws PortalException {
116: if (positionSet == null || positionSet.getFirstChild() == null)
117: return;
118:
119: ArrayList order = new ArrayList();
120:
121: applyOrdering(order, compViewParent, positionSet);
122: applyNoReparenting(order, compViewParent, positionSet);
123: applyNoHopping(order, compViewParent, positionSet);
124: applyLowerPrecedence(order, compViewParent, positionSet);
125: evaluateAndApply(order, compViewParent, positionSet, result);
126: }
127:
128: /**
129: This method determines if applying all of the positioning rules and
130: restrictions ended up making changes to the compViewParent or the
131: original position set. If changes are applicable to the CVP then they
132: are applied. If the position set changed then the original stored in the
133: PLF is updated.
134: */
135: static void evaluateAndApply(ArrayList order,
136: Element compViewParent, Element positionSet,
137: IntegrationResult result) throws PortalException {
138: adjustPositionSet(order, positionSet, result);
139:
140: if (hasAffectOnCVP(order, compViewParent)) {
141: applyToNodes(order, compViewParent);
142: result.changedILF = true;
143: }
144: }
145:
146: /**
147: This method trims down the position set to the position directives on
148: the node info elements still having a position directive. Any directives
149: that violated restrictions were removed from the node info objects so
150: the position set should be made to match the order of those still
151: having one.
152: */
153: static void adjustPositionSet(ArrayList order, Element positionSet,
154: IntegrationResult result) {
155: Node nodeToMatch = positionSet.getFirstChild();
156: Element nodeToInsertBefore = positionSet.getOwnerDocument()
157: .createElement("INSERT_POINT");
158: positionSet.insertBefore(nodeToInsertBefore, nodeToMatch);
159:
160: for (Iterator iter = order.iterator(); iter.hasNext();) {
161: NodeInfo ni = (NodeInfo) iter.next();
162:
163: if (ni.positionDirective != null) {
164: // found one check it against the current one in the position
165: // set to see if it is different. If so then indicate that
166: // something (the position set) has changed in the plf
167: if (ni.positionDirective != nodeToMatch)
168: result.changedPLF = true;
169:
170: // now bump the insertion point forward prior to
171: // moving on to the next position node to be evaluated
172: if (nodeToMatch != null)
173: nodeToMatch = nodeToMatch.getNextSibling();
174:
175: // now insert it prior to insertion point
176: positionSet.insertBefore(ni.positionDirective,
177: nodeToInsertBefore);
178: }
179: }
180:
181: // now for any left over after the insert point remove them.
182:
183: while (nodeToInsertBefore.getNextSibling() != null)
184: positionSet
185: .removeChild(nodeToInsertBefore.getNextSibling());
186:
187: // now remove the insertion point
188: positionSet.removeChild(nodeToInsertBefore);
189: }
190:
191: /**
192: This method compares the children by id in the order list with
193: the order in the compViewParent's ui visible children and returns true
194: if the ordering differs indicating that the positioning if needed.
195: */
196: static boolean hasAffectOnCVP(ArrayList order,
197: Element compViewParent) {
198: if (order.size() == 0)
199: return false;
200:
201: int idx = 0;
202: Element child = (Element) compViewParent.getFirstChild();
203: NodeInfo ni = (NodeInfo) order.get(idx);
204:
205: if (child == null && ni != null) // most likely nodes to be pulled in
206: return true;
207:
208: while (child != null) {
209: if (child.getAttribute("hidden").equals("false")
210: && (!child.getAttribute("chanID").equals("") || child
211: .getAttribute("type").equals("regular"))) {
212: if (ni.id.equals(child.getAttribute(Constants.ATT_ID))) {
213: if (idx >= order.size() - 1) // at end of order list
214: return false;
215:
216: ni = (NodeInfo) order.get(++idx);
217: } else
218: // if not equal then return true
219: return true;
220: }
221: child = (Element) child.getNextSibling();
222: }
223: if (idx < order.size())
224: return true; // represents nodes to be pulled in
225: return false;
226: }
227:
228: /**
229: This method applies the ordering specified in the passed in order list
230: to the child nodes of the compViewParent. Nodes specified in the list
231: but located elsewhere are pulled in.
232: */
233: static void applyToNodes(ArrayList order, Element compViewParent) {
234: // first set up a bogus node to assist with inserting
235: Node insertPoint = compViewParent.getOwnerDocument()
236: .createElement("bogus");
237: Node first = compViewParent.getFirstChild();
238:
239: if (first != null)
240: compViewParent.insertBefore(insertPoint, first);
241: else
242: compViewParent.appendChild(insertPoint);
243:
244: // now pass through the order list inserting the nodes as you go
245: for (int i = 0; i < order.size(); i++)
246: compViewParent.insertBefore(((NodeInfo) order.get(i)).node,
247: insertPoint);
248:
249: compViewParent.removeChild(insertPoint);
250: }
251:
252: /**
253: This method is responsible for preventing nodes with lower precedence
254: from being located to the left (lower sibling order) of nodes having a
255: higher precedence and moveAllowed="false".
256: */
257: static void applyLowerPrecedence(ArrayList order,
258: Element compViewParent, Element positionSet) {
259: for (int i = 0; i < order.size(); i++) {
260: NodeInfo ni = (NodeInfo) order.get(i);
261: if (ni.node.getAttribute(Constants.ATT_MOVE_ALLOWED)
262: .equals("false")) {
263: for (int j = 0; j < i; j++) {
264: NodeInfo lefty = (NodeInfo) order.get(j);
265: if (lefty.precedence == null
266: || lefty.precedence
267: .isLessThan(ni.precedence)) {
268: order.remove(j);
269: order.add(i, lefty);
270: }
271: }
272: }
273: }
274: }
275:
276: /**
277: This method is responsible for preventing nodes with identical
278: precedence in the same parent from hopping over each other so that a
279: layout fragment can lock two tabs that are next to each other and they
280: can only be separated by tabs with higher precedence.
281:
282: If this situation is detected then the positioning of all nodes
283: currently in the compViewParent is left as they are found in the CVP
284: with any nodes brought in from
285: other parents appended at the end with their relative order preserved.
286: */
287: static void applyNoHopping(ArrayList order, Element compViewParent,
288: Element positionSet) {
289: if (isIllegalHoppingSpecified(order) == true) {
290: ArrayList cvpNodeInfos = new ArrayList();
291:
292: // pull those out of the position list from the CVP
293: for (int i = order.size() - 1; i >= 0; i--)
294: if (((NodeInfo) order.get(i)).indexInCVP != -1)
295: cvpNodeInfos.add(order.remove(i));
296:
297: // what is left is coming from other parents. Now push them back in
298: // in the order specified in the CVP
299:
300: Object[] nodeInfos = cvpNodeInfos.toArray();
301: Arrays.sort(nodeInfos, new NodeInfoComparator());
302: List list = Arrays.asList(nodeInfos);
303: order.addAll(0, list);
304: }
305: }
306:
307: /**
308: This method determines if any illegal hopping is being specified.
309: To determine if the positioning is specifying an ordering that will
310: result in hopping I need to determine for each node n in the list if
311: any of the nodes to be positioned to its right currently lie to its
312: left in the CVP and have moveAllowed="false" and have the same
313: precedence or if any of the nodes to be positioned to its left currently
314: lie to its right in the CVP and have moveAllowed="false" and have the
315: same precedence.
316:
317: */
318: static boolean isIllegalHoppingSpecified(ArrayList order) {
319: for (int i = 0; i < order.size(); i++) {
320: NodeInfo ni = (NodeInfo) order.get(i);
321:
322: // look for move restricted nodes
323: if (!ni.node.getAttribute(Constants.ATT_MOVE_ALLOWED)
324: .equals("false"))
325: continue;
326:
327: // now check nodes in lower position to see if they "hopped" here
328: // or if they have similar precedence and came from another parent.
329:
330: for (int j = 0; j < i; j++) {
331: NodeInfo niSib = (NodeInfo) order.get(j);
332:
333: // skip lower precedence nodes from this parent. These will get
334: // bumped during the lower precedence check
335: if (niSib.precedence == Precedence.getUserPrecedence())
336: continue;
337:
338: if (niSib.precedence.isEqualTo(ni.precedence)
339: && (niSib.indexInCVP == -1 || // from another parent
340: ni.indexInCVP < niSib.indexInCVP)) // niSib hopping left
341: return true;
342: }
343:
344: // now check upper positioned nodes to see if they "hopped"
345:
346: for (int j = i + 1; j < order.size(); j++) {
347: NodeInfo niSib = (NodeInfo) order.get(j);
348:
349: // ignore nodes from other parents and user precedence nodes
350: if (niSib.indexInCVP == -1
351: || niSib.precedence == Precedence
352: .getUserPrecedence())
353: continue;
354:
355: if (ni.indexInCVP > niSib.indexInCVP && // niSib hopped right
356: niSib.precedence.isEqualTo(ni.precedence))
357: return true;
358: }
359: }
360: return false;
361: }
362:
363: /**
364: This method scans through the nodes in the ordered list and identifies
365: those that are not in the passed in compViewParent. For those it then
366: looks in its current parent and checks to see if there are any down-
367: stream (higher sibling index) siblings that have moveAllowed="false".
368: If any such sibling is found then the node is not allowed to be
369: reparented and is removed from the list.
370: */
371: static void applyNoReparenting(ArrayList order,
372: Element compViewParent, Element positionSet) {
373: int i = 0;
374: while (i < order.size()) {
375: NodeInfo ni = (NodeInfo) order.get(i);
376: if (!ni.node.getParentNode().equals(compViewParent)) {
377: ni.differentParent = true;
378: if (isNotReparentable(ni)) {
379: // this node should not be reparented. If it was placed
380: // here by way of a position directive then delete that
381: // directive out of the ni and posSet will be updated later
382: ni.positionDirective = null;
383:
384: // now we need to remove it from the ordering list but
385: // skip incrementing i, deleted ni now filled by next ni
386: order.remove(i);
387: continue;
388: }
389: }
390: i++;
391: }
392: }
393:
394: /**
395: Return true if the passed in node or any of its up-stream (higher index
396: siblings have moveAllowed="false".
397: */
398: private static boolean isNotReparentable(NodeInfo ni) {
399: if (ni.node.getAttribute(Constants.ATT_MOVE_ALLOWED).equals(
400: "false"))
401: return true;
402:
403: Precedence nodePrec = ni.precedence;
404: Element node = (Element) ni.node.getNextSibling();
405:
406: while (node != null) {
407: if (node.getAttribute(Constants.ATT_MOVE_ALLOWED).equals(
408: "false")) {
409: Precedence p = Precedence.newInstance(node
410: .getAttribute(Constants.ATT_FRAGMENT));
411: if (nodePrec.isEqualTo(p))
412: return true;
413: }
414: node = (Element) node.getNextSibling();
415: }
416: return false;
417: }
418:
419: /**
420: This method assembles in the passed in order object a list of NodeInfo
421: objects ordered first by those specified in the position set and whose
422: nodes still exist in the composite view and then by any remaining
423: children in the compViewParent.
424: */
425: static void applyOrdering(ArrayList order, Element compViewParent,
426: Element positionSet) {
427: // first pull out all visible channel or visible folder children and
428: // put their id's in a list of available children and record their
429: // relative order in the CVP.
430:
431: ArrayList available = new ArrayList();
432:
433: Element child = (Element) compViewParent.getFirstChild();
434: Element next = null;
435: int indexInCVP = 0;
436:
437: while (child != null) {
438: next = (Element) child.getNextSibling();
439:
440: if (child.getAttribute("hidden").equals("false")
441: && (!child.getAttribute("chanID").equals("") || child
442: .getAttribute("type").equals("regular")))
443: available.add(new NodeInfo(child, indexInCVP++));
444: child = next;
445: }
446:
447: // now fill the order list using id's from the position set if nodes
448: // having those ids exist in the composite view. Otherwise discard
449: // that position directive. As they are added to the list remove them
450: // from the available nodes in the parent.
451:
452: Document CV = compViewParent.getOwnerDocument();
453: Element directive = (Element) positionSet.getFirstChild();
454:
455: while (directive != null) {
456: next = (Element) directive.getNextSibling();
457:
458: // id of child to move is in the name attrib on the position nodes
459: String id = directive.getAttribute("name");
460: child = CV.getElementById(id);
461:
462: if (child != null) {
463: // look for the NodeInfo for this node in the available
464: // nodes and if found use that one. Otherwise use a new that
465: // does not include an index in the CVP parent. In either case
466: // indicate the position directive responsible for placing this
467: // NodeInfo object in the list.
468:
469: int idx = 0;
470: boolean found = false;
471:
472: while (!found && idx < available.size()) {
473: if (((NodeInfo) available.get(idx)).node == child)
474: found = true;
475: else
476: idx++;
477: }
478: NodeInfo ni = (found ? (NodeInfo) available.remove(idx)
479: : new NodeInfo(child));
480: ni.positionDirective = directive;
481: order.add(ni);
482: }
483: directive = next;
484: }
485:
486: // now append any remaining ids from the available list maintaining
487: // the order that they have there.
488:
489: for (int i = 0; i < available.size(); i++)
490: order.add(available.get(i));
491: }
492:
493: /**
494: This method updates the positions recorded in a position set to reflect
495: the ids of the nodes in the composite view of the layout. Any position
496: nodes already in existence are reused to reduce database interaction
497: needed to generate a new ID attribute. If any are left over after
498: updating those position elements are removed. If no position set existed
499: a new one is created for the parent. If no ILF nodes are found in the
500: parent node then the position set as a whole is reclaimed.
501: */
502: public static void updatePositionSet(Element compViewParent,
503: Element plfParent, IPerson person) throws PortalException {
504: if (LOG.isDebugEnabled())
505: LOG.debug("Updating Position Set");
506:
507: if (compViewParent.getChildNodes().getLength() == 0) {
508: // no nodes to position. if set exists reclaim the space.
509: if (LOG.isDebugEnabled())
510: LOG.debug("No Nodes to position");
511: Element positions = getPositionSet(plfParent, person, false);
512: if (positions != null)
513: plfParent.removeChild(positions);
514: return;
515: }
516: Element posSet = (Element) getPositionSet(plfParent, person,
517: true);
518: Element position = (Element) posSet.getFirstChild();
519: Element viewNode = (Element) compViewParent.getFirstChild();
520: boolean ilfNodesFound = false;
521:
522: while (viewNode != null) {
523: String ID = viewNode.getAttribute(Constants.ATT_ID);
524: String channelId = viewNode
525: .getAttribute(Constants.ATT_CHANNEL_ID);
526: String type = viewNode.getAttribute(Constants.ATT_TYPE);
527: String hidden = viewNode.getAttribute(Constants.ATT_HIDDEN);
528:
529: if (ID.startsWith(Constants.FRAGMENT_ID_USER_PREFIX))
530: ilfNodesFound = true;
531:
532: if (!channelId.equals("") || // its a channel node or
533: (type.equals("regular") && // a regular, visible folder
534: hidden.equals("false"))) {
535: if (position != null)
536: position.setAttribute(Constants.ATT_NAME, ID);
537: else
538: position = createAndAppendPosition(ID, posSet,
539: person);
540: position = (Element) position.getNextSibling();
541: }
542: viewNode = (Element) viewNode.getNextSibling();
543: }
544:
545: if (ilfNodesFound == false) // only plf nodes, no pos set needed
546: plfParent.removeChild(posSet);
547: else {
548: // reclaim any leftover positions
549: while (position != null) {
550: Element nextPos = (Element) position.getNextSibling();
551: posSet.removeChild(position);
552: position = nextPos;
553: }
554: }
555: }
556:
557: /**
558: This method locates the position set element in the child list of the
559: passed in plfParent or if not found it will create one automatically
560: and return it if the passed in create flag is true.
561: */
562: private static Element getPositionSet(Element plfParent,
563: IPerson person, boolean create) throws PortalException {
564: Node child = plfParent.getFirstChild();
565:
566: while (child != null) {
567: if (child.getNodeName().equals(Constants.ELM_POSITION_SET))
568: return (Element) child;
569: child = child.getNextSibling();
570: }
571: if (create == false)
572: return null;
573:
574: String ID = null;
575:
576: try {
577: ID = getDLS().getNextStructDirectiveId(person);
578: } catch (Exception e) {
579: throw new PortalException("Exception encountered while "
580: + "generating new position set node "
581: + "Id for userId=" + person.getID(), e);
582: }
583: Document plf = plfParent.getOwnerDocument();
584: Element positions = plf
585: .createElement(Constants.ELM_POSITION_SET);
586: positions.setAttribute(Constants.ATT_TYPE,
587: Constants.ELM_POSITION_SET);
588: positions.setAttribute(Constants.ATT_ID, ID);
589: plfParent.appendChild(positions);
590: return positions;
591: }
592:
593: /**
594: Create, append to the passed in position set, and return a position
595: element that references the passed in elementID.
596: */
597: private static Element createAndAppendPosition(String elementID,
598: Element positions, IPerson person) throws PortalException {
599: if (LOG.isDebugEnabled())
600: LOG.debug("Adding Position Set entry " + elementID + ".");
601:
602: String ID = null;
603:
604: try {
605: ID = getDLS().getNextStructDirectiveId(person);
606: } catch (Exception e) {
607: throw new PortalException("Exception encountered while "
608: + "generating new position node "
609: + "Id for userId=" + person.getID(), e);
610: }
611: Document plf = positions.getOwnerDocument();
612: Element position = plf.createElement(Constants.ELM_POSITION);
613: position.setAttribute(Constants.ATT_TYPE,
614: Constants.ELM_POSITION);
615: position.setAttribute(Constants.ATT_ID, ID);
616: position.setAttributeNS(Constants.NS_URI, Constants.ATT_NAME,
617: elementID);
618: positions.appendChild(position);
619: return position;
620: }
621:
622: static class NodeInfoComparator implements Comparator {
623: public int compare(Object o1, Object o2) {
624: return ((NodeInfo) o1).indexInCVP
625: - ((NodeInfo) o2).indexInCVP;
626: }
627: }
628:
629: }
|