cancel
Showing results for 
Search instead for 
Did you mean: 

Merge of Nodes provided by an External Navigation Connector

Former Member
0 Kudos

Hi,

I have implemented an (External) Navigation Connector following the advices given [here|http://help.sap.com/saphelp_nw70/helpdata/EN/45/004eb029a60484e10000000a155369/frameset.htm]. This Connector works well when being used inside the Role Connector by setting the attribute com.sap.portal.navigation.externalConnectorKey ([com.sapportals.portal.navigation.INavigationConstants#EXTERNAL_CONNECTOR_KEY|http://help.sap.com/javadocs/NW04s/current/ep/com/sapportals/portal/navigation/INavigationConstants.html#EXTERNAL_CONNECTOR_KEY]). However, I encountered problems when trying to merge nodes delivered by this connector within "normal" nodes (based on the RoleFolder PCD context). When I try to load the children of the merged node dynamically, the Detailed Navigation Tree "freezes" with the Loading... message. First of all I wanted to document the problem and show my solution, so that other users running into the same problem can catch up on this. Iu2019m not quite sure yet if its a bug or simply not (yet) supported, I used NW AS 7.0 SPS15.

I have the following nodes configured inside my sample role, FKRole:

FKRole 
|- extNode
|- test1.1
   |- test1.1.1
   |- test1.1.2
      |- test1.1.2.1
      |- test1.1.2.2

Then I implemented a Connector simulating nodes, registered it with the prefix MOCK and configured it at extNode with the key MOCK://testKey. After this configuration, the tree (fully expanded) looks as following:

FKRole 
|- extNode
   |- node1
      |- node1.1
      |- node1.2
   |- node2
      |- node2.1
      |- node2.2
|- test1.1
   |- test1.1.1
   |- test1.1.2
      |- test1.1.2.1
      |- test1.1.2.2

First of all, I merged node extNode and test1.1, setting the com.sap.portal.navigation.MergeId ([com.sap.portal.pcm.attributes.IAttrPcmNavigation#ATTRIBUTE_MERGEID|http://help.sap.com/javadocs/NW04s/current/ep/com/sap/portal/pcm/attributes/IAttrPcmNavigation.html#ATTRIBUTE_MERGEID]) on both nodes to mergeRoot. I set com.sap.portal.navigation.MergePriority ([com.sap.portal.pcm.attributes.IAttrPcmNavigation#ATTRIBUTE_MERGE_PRIORITY|http://help.sap.com/javadocs/NW04s/current/ep/com/sap/portal/pcm/attributes/IAttrPcmNavigation.html#ATTRIBUTE_MERGE_PRIORITY]) attribute of test1.1 to 49D since the default value of the nodes provided by the MOCK connector is 50D.

The result looks like this:

FKRole 
|- test1.1
   |- node1
      |- node1.1
      |- node1.2
   |- node2
      |- node2.1
      |- node2.2
   |- test1.1.1
   |- test1.1.2
      |- test1.1.2.1
      |- test1.1.2.2

This behavior is correct, since the [com.sapportals.portal.navigation.InavigationConnectorNode#getPriority()|http://help.sap.com/javadocs/NW04S/current/ep/com/sapportals/portal/navigation/INavigationConnectorNode.html#getPriority()] defaults to 50D as mentioned above. To proof this, I set the com.sap.portal.navigation.Priority ([com.sap.portal.pcm.attributes.IAttrPcmNavigation# ATTRIBUTE_SORT_PRIORITY|http://help.sap.com/javadocs/NW04s/current/ep/com/sap/portal/pcm/attributes/IAttrPcmNavigation.html#ATTRIBUTE_SORT_PRIORITY]) attribute of test1.1.2 to 49D, the result looks like this:

FKRole 
|- test1.1
   |- test1.1.2
      |- test1.1.2.1
      |- test1.1.2.2
   |- node1
      |- node1.1
      |- node1.2
   |- node2
      |- node2.1
      |- node2.2
   |- test1.1.1

So far so good, now to the root crux. I merge test1.1.2 with node2 (which is provided by the MOCK connector). Therefore I return a merge id related to the nodeu2019s id (which is also its title) at [com.sapportals.portal.navigation.InavigationConnectorNode#getMergeID()|http://help.sap.com/javadocs/NW04S/current/ep/com/sapportals/portal/navigation/INavigationConnectorNode.html#getMergeID()]. My notation for this is merge_<id>, e.g. merge_node2. Then is set this value to com.sap.portal.navigation.MergeId attribute of test1.1.2. Since I also wanted test1.1.2 to be the u201Cdominantu201D I set the value of com.sap.portal.navigation.MergePriority attribute to 49D, since the connector nodes also default to 50D when being requested with that key over [com.sapportals.portal.navigation.INavigationConnectorNode#getAttributeValue(String, Locale|http://help.sap.com/javadocs/NW04S/current/ep/com/sapportals/portal/navigation/INavigationConnectorNode.html#getAttributeValue(java.lang.String,%20java.util.Locale)]. The result looks like this (collapsed):

FKRole 
|- test1.1
   |- test1.1.2 (+)
   |- node1 (+)
   |- test1.1.1

Accepted Solutions (0)

Answers (5)

Answers (5)

Former Member
0 Kudos

Hi,

After opening an OSS ticket, the problem is acknowledged as Bug by SAP and will get fixed in NW 7.0 SPS20 ([which is not scheduled yet|https://service.sap.com/~form/sapnet?_SHORTKEY=01100035870000722640&_SCENARIO=01100035870000000202]). My first four postings were edited by an administrator(?) so that the formatting is now destroyed. I have neither time nor desire to fix this by myself besides I can't edit the first post.

BR, Fabian

Former Member
0 Kudos

Hi,

I didn't find any notes on this issue, but I did a source code comparison between AS7.00 SPS16 (the version where I encountered the issue), AS7.00 SPS18 and AS7.01 SPS3. There's no difference between the com.sapportals.portal.navigation.NavigationService version of AS7.00 SPS16 and AS7.00 SPS18.

They did make some changes in AS7.01 SPS3 to the NavigationService, but I think this is primary for [this|http://help.sap.com/saphelp_nw70ehp1/helpdata/en/fd/b62e4d0da746d9a09aa8d7643989ae/frameset.htm] feature. There's a getCacheTTLByConnector(String) which is not used internally, I guess it's used by the CacheCleaner. And there's doAllFilters(Hashtable, NavigationNodes) which seems to apply some filters after initial nodes retrieval. However, they did change the way they merge nodes, but I think this is for refactoring reasons.The call on com.sapportals.portal.navigation.NavigationNode#mergeWith(NavigationNode) in getNode(Hashtable, String) is replaced by a method which was already used in AS7.00 SPS16 called mergeNodes(NavigationNode, NavigationNode, NavigationNodes) which also merges nodes, but also updates the given nodes list (third parameter, NavigationNodes=ArrayList). They changed the mandatory third parameter to be optional (=null), for which they added the following check (line 1276, decompiled with jad1.5.8g):

if(nodes != null)
            {
                int index = nodes.indexOf(originalNode);
                if(index != -1)
                    nodes.set(index, newNode);
            }

which was before (AS7.00 SPS16):

int index = nodes.indexOf(originalNode);
            if(index != -1)
                nodes.set(index, newNode);

I think the behavior regarding the issue described above didn't change. But the behavior could have changed in the calling component, DTN. I'll open a ticket now and report the result in this thread.

Best regards,

Fabian

Former Member
0 Kudos

I fixed this behavior with the following code:

	public INavigationNode getNode(Hashtable environment, String nodeName) {
		nodeName = getNavigationNodeOriginalName(environment, nodeName);
		if (nodeName == null)
			return getFirstNode(environment);
		try {
			INavigationRedirectorResult result = redirect(nodeName, environment);
			if (result.getPrefix().equals("navext"))
				nodeName = result.getPrefix() + result.getURL();
			else
				nodeName = result.getPrefix() + "://" + result.getURL();
		} catch (NamingException e) {
			mm_logger.traceThrowableT(TracingConsts.DEBUG,
					"NavigationService.getNode: Fail to redirect navigation target "
							+ nodeName, e);
		}

		boolean isExternal = isExternalNode(nodeName);
		Matcher matcher = PATTERN_MERGE.matcher(nodeName);
		if (matcher.find()) {
			String mergeGroup = matcher.group(1);
			NavigationNode mergedNode = null;
			String[] mergeSegments = segmentMergeString(mergeGroup);
			for (int i = 0; i < mergeSegments.length; i++) {
				String mergeSegment = mergeSegments<i>;
				boolean externalNode = isExternalNode(mergeSegment);
				NavigationNode segmentNode = null;
				if (externalNode) {
					segmentNode = getExtNavNode(environment, mergeSegment);
				} else {
					segmentNode = getNavNode(environment, mergeSegment);
				}
				if (mergedNode == null) {
					mergedNode = segmentNode;
				} else {
					mergedNode.mergeWith(segmentNode);
				}
			}
			return mergedNode;
		}
		if (isExternal)
			return getExtNavNode(environment, nodeName);
		else
			return getNavNode(environment, nodeName);
	}

	private static final Pattern PATTERN_NODES = Pattern.compile(
			"(([^\\(\\)\\|]+)\\|)|([^\\(]*\\([^\\)]*\\))|([^\\(\\)\\|]*$)",
			Pattern.CANON_EQ);

	private static final Pattern PATTERN_MERGE = Pattern.compile(
			"merge\\((.*)\\)", Pattern.CANON_EQ);

	/**
	 * <p>
	 * Segments a string consisting of several node ids separated by the pipe
	 * (|) character. This notation is normally used by merged nodes in order to
	 * separate the ids. But in the case of a node provided by an external
	 * navigation connector, it is also used to separate the virtual from the
	 * physical part of such a node.<br/>
	 * <b>Can't handle merge(...) notation, remove before call.</b>
	 * </p>
	 * 
	 * <p>
	 * E.g.
	 * <code>ROLES://portal_content/fkrole/test1/test1.1/test1.1.2|
	 * navext(ROLES://portal_content/fkrole/test1/extNode|MOCK://test_key/~//node2)</code>
	 * is splitted into two strings,
	 * <code>ROLES://portal_content/fkrole/test1/test1.1/test1.1.2</code> and
	 * 
	 * <code>navext(ROLES://portal_content/fkrole/test1/extNode|MOCK://test_key/~//node2)</code>
	 * .
	 * </p>
	 * 
	 * @param mergeString
	 *            the string to be segmented
	 * @return the segmented strings as an array
	 */
	private String[] segmentMergeString(String mergeString) {
		List segmentsList = new LinkedList();
		Matcher mergeStringMatcher = PATTERN_NODES.matcher(mergeString);
		while (mergeStringMatcher.find()) {
			String group0 = mergeStringMatcher.group(0);
			if (group0 == null || "".equals(group0))
				continue;
			String group1 = mergeStringMatcher.group(1);
			String group2 = mergeStringMatcher.group(2);
			if (group2 != null) {
				segmentsList.add(group2);
				continue;
			} else if (group1 != null) {
				segmentsList.add(group1);
			} else {
				segmentsList.add(group0);
			}
		}
		String[] segments = new String[segmentsList.size()];
		return (String[]) segmentsList.toArray(segments);
	}

Please donu2019t blame me for the regex, Iu2019m not good at this. However, after exchanging the code, the navigation looks like this:

FKRole 
|- test1.1
   |- test1.1.2
      |- node2.1
      |- node2.2
      |- test1.1.2.1
      |- test1.1.2.2
   |- node1
      |- node1.1
      |- node1.2
   |- test1.1.1

So after taking the navext-function into account I can merge nodes provided by an external navigation connector with other nodes provided by the ROLES connector. I will now investigate the issue further to clarify if this is a bug, and check if it is not fixed yet. If so, I will open an OSS ticket and keep this thread up-to-date. Furthermore, I appreciate any input on this issue, primary documentation different from [this|http://help.sap.com/saphelp_nw70ehp1/helpdata/en/42/fd19d73b676fb4e10000000a11466f/frameset.htm] explaining how to implement an external navigation connector and specifying the (silent) contracts of that framework. I also would like to know if other users ran into this problem or could successfully merge nodes provided by an external navigation connector.

If you need the above code in form of a compiled com.sapportals.portal.navigation.NavigationService Iu2019ll provide it without any guarantee on email request.

Best regards,

Fabian

Former Member
0 Kudos

Here is the original source code of the com.sapportals.portal.navigation.NavigationService (since it is decompiled, it is truly not like a human would write it):

    public INavigationNode getNode(Hashtable environment, String nodeName)
    {
        nodeName = getNavigationNodeOriginalName(environment, nodeName);
        if(nodeName == null)
            return getFirstNode(environment);
        try
        {
            INavigationRedirectorResult result = redirect(nodeName, environment);
            if(result.getPrefix().equals("navext"))
                nodeName = result.getPrefix() + result.getURL();
            else
                nodeName = result.getPrefix() + "://" + result.getURL();
        }
        catch(NamingException e)
        {
            mm_logger.traceThrowableT(TracingConsts.DEBUG, "NavigationService.getNode: "+
"Fail to redirect navigation target " + nodeName, e);
        }
        boolean isMerged = nodeName.substring(0, "merge(".length()).equalsIgnoreCase("merge(");
        boolean isExternal = isExternalNode(nodeName);
        if(isMerged)
        {
            NavigationNode mergedNode = null;
            int startOfCur = "merge(".length();
            int endOfCur = nodeName.indexOf("|");
            if(isExternalNode(nodeName.substring(startOfCur, endOfCur)))
                mergedNode = getExtNavNode(environment, nodeName);
            else
                mergedNode = getNavNode(environment, nodeName.substring(startOfCur, endOfCur));
            for(startOfCur = endOfCur + 1; !nodeName.substring(endOfCur).equals(")"); startOfCur = endOfCur + 1)
            {
                endOfCur = nodeName.substring(startOfCur).indexOf("|");
                if(endOfCur == -1)
                    endOfCur = startOfCur + nodeName.substring(startOfCur).lastIndexOf(")");
                else
                    endOfCur += startOfCur;
                if(!isExternalNode(nodeName.substring(startOfCur, endOfCur)))
                    mergedNode.mergeWith(getNavNode(environment, nodeName.substring(startOfCur, endOfCur)));
                else
                    mergedNode.mergeWith(getExtNavNode(environment, nodeName.substring(startOfCur, endOfCur)));
            }

            return mergedNode;
        }
        if(isExternal)
            return getExtNavNode(environment, nodeName);
        else
            return getNavNode(environment, nodeName);
    }

The problem now is that if one of the merged nodes is an external node. Then the pipe (|) character is also used to separate the physical from the virtual part, like in

navext(ROLES://portal_content/fkrole/test1/extNode|MOCK://test_key/~//node2)

The for-loop above will tokenize strings with the pipe (|) character, therefore the getExtNavNode gets called with

navext(ROLES://portal_content/de.reddot.sap.fk/de.reddot.sap.roles.fkrole/test1/extNode

which of course canu2019t retrieve the node. So the problem is that the method should also take navext into account.

Former Member
0 Kudos

This is perfectly correct (the is an expandable node in my notation), because when I change com.sap.portal.navigation.MergePriority back to 51D test1.1.2 is shown as node2. That means that the test1.1.2 and node2 nodes are merged. However, when I expand test1.1.2 in the Detailed Navigation Tree component, it doesnu2019t show the children of the merged nodes. It just freezes in the u201CLoadingu2026u201D state as mentioned above.

This behavior is typical when an exception occurred inside the dynamic load. The defaultTrace tells me about a

java.lang.StringIndexOutOfBoundsException

in line 433 at

com.sapportals.portal.navigation.NavigationService.getExtNavNode(Hashtable, String)

The line number doesnu2019t match the SAP provided code, since I already decompiled and recompiled with debug flag. The following statement inside the getExtNavNode method causes the exception:

String curr = tmpName.substring("navext(".length(), tmpName.indexOf("|"));

where tmpName is

navext(ROLES://portal_content/fkrole/test1/extNode

in this case. The relevant caller stack is

CachedNavigationService(NavigationService).getExtNavNode(Hashtable, String) line: 433	
CachedNavigationService(NavigationService).getNode(Hashtable, String) line: 419	
CachedNavigationService.getNode(Hashtable, String) line: 130	
DetailedNavigationTree.setDynamicLoadParentNode(IPortalComponentRequest, String) line: 414	
DetailedNavigationTree.doOnNodeReady(IPortalComponentRequest, IEvent) line: 172	
DetailedNavigationTree(AbstractPortalComponent).handleEvent(IPortalComponentRequest, IEvent) line: 388	
ComponentNode.handleEvent(IEvent) line: 141

where com.sapportals.portal.navigation.cache.CachedNavigationService and com.sapportals.portal.navigation.DetailedNavigationTree are also decompiled and recompiled with debug, so their line numbers donu2019t count too.

Now when I see the caller stack, especially the setDynamicLoadParentNode method I can guess without investigating much further that somehow the parent node of the currently expanded test1.1.2 is being acquired. Now when I set a breakpoint before the above mentioned statement and go back to the calling getNode method I see the following nodeName (the linebreak is artificial)

merge(ROLES://portal_content/fkrole/test1/test1.1/test1.1.2|
navext(ROLES://portal_content/fkrole/test1/extNode|MOCK://test_key/~//node2))

which is correct to my eyes, because it identifies a merged node. The first part of the merged node is the RoleFolder node

ROLES://portal_content/fkrole/test1/test1.1/test1.1.2

and the second part is the external navigation connector node, which is represented by two parts, one represents the RoleFolder node where the connector is initialized at (I call that physical part)

ROLES://portal_content/fkrole/test1/extNode

and one

MOCK://test_key/~//node2

represents the nodeName which is used to initialize the connector and retrieve the nodes (I call this [virtual part|http://help.sap.com/saphelp_nw70ehp1/helpdata/en/45/003c4b3be30485e10000000a155369/frameset.htm]). MOCK is the prefix under which the connector is registered, test_key is the initialization key (which is implementation specific, and could identify a node f.e., but in my case it has no meaning), /~/ is the separator to the connectorNodeName, which is externalized in [com.sapportals.portal.navigation.INavigationConstants#KEY_SEPERATOR|http://help.sap.com/javadocs/NW04s/current/ep/constant-values.html#com.sapportals.portal.navigation.INavigationConstants.KEY_SEPERATOR] (the javadoc is not up-to-date, it mentiones ~ but if you decompile INavigationConstants you see /~/), and /node2, which is the connectorNodeName, the path to the node inside my connectors hierarchy (I use the com.sapportals.portal.navigation.DefaultNavigationNamingHandler, but return null for the parents of the inital nodes as mentioned in the documentation).

The CachedNavigationService(NavigationService).getNode(Hashtable, String) method seems to decompose the given nodeName into the two merge parts, identifythe two nodes and merge them together.