/******************************************************************************* * Copyright (c) 2007, 2014 compeople AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * compeople AG - initial API and implementation *******************************************************************************/ package org.eclipse.riena.navigation.model; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import org.osgi.service.log.LogService; import org.eclipse.core.runtime.Assert; import org.eclipse.equinox.log.Logger; import org.eclipse.riena.core.Log4r; import org.eclipse.riena.core.util.StringUtils; import org.eclipse.riena.internal.navigation.Activator; import org.eclipse.riena.navigation.IAssemblerProvider; import org.eclipse.riena.navigation.IGenericNavigationAssembler; import org.eclipse.riena.navigation.INavigationAssembler; import org.eclipse.riena.navigation.INavigationNode; import org.eclipse.riena.navigation.INavigationNodeProvider; import org.eclipse.riena.navigation.NavigationArgument; import org.eclipse.riena.navigation.NavigationNodeId; import org.eclipse.riena.navigation.NodePositioner; import org.eclipse.riena.navigation.StartupNodeInfo; import org.eclipse.riena.navigation.StartupNodeInfo.Level; import org.eclipse.riena.navigation.extension.ICommonNavigationAssemblyExtension; import org.eclipse.riena.navigation.extension.INavigationAssembly2Extension; import org.eclipse.riena.navigation.extension.INode2Extension; import org.eclipse.riena.ui.core.uiprocess.UIExecutor; /** * This class provides navigation nodes that are defined by assemlies2 extensions. */ public abstract class AbstractSimpleNavigationNodeProvider implements INavigationNodeProvider, IAssemblerProvider { private final static Logger LOGGER = Log4r.getLogger(Activator.getDefault(), AbstractSimpleNavigationNodeProvider.class); private static Random random = null; private final Map<String, INavigationAssembler> assemblyId2AssemblerCache = new HashMap<String, INavigationAssembler>(); /** * {@inheritDoc} */ public List<StartupNodeInfo> getSortedStartupNodeInfos() { final List<StartupNodeInfo> startups = new ArrayList<StartupNodeInfo>(); for (final INavigationAssembler assembler : getNavigationAssemblers()) { if (assembler.getStartOrder() > 0) { final StartupNodeInfo startupNodeInfo = createStartupSortable(assembler, assembler.getStartOrder()); if (startupNodeInfo != null) { startups.add(startupNodeInfo); } } } Collections.sort(startups); return startups; } private StartupNodeInfo createStartupSortable(final INavigationAssembler assembler, final Integer sequence) { final INavigationAssembly2Extension assembly = assembler.getAssembly(); if (assembly == null) { return null; } String id = getTypeId(assembly.getSubApplications()); if (id != null) { return new StartupNodeInfo(Level.SUBAPPLICATION, sequence, id); } id = getTypeId(assembly.getModuleGroups()); if (id != null) { return new StartupNodeInfo(Level.MODULEGROUP, sequence, id); } id = getTypeId(assembly.getModules()); if (id != null) { return new StartupNodeInfo(Level.MODULE, sequence, id); } id = getTypeId(assembly.getSubModules()); if (id != null) { return new StartupNodeInfo(Level.SUBMODULE, sequence, id); } id = assembler.getId(); Assert.isNotNull(assembly.getNavigationAssembler(), "Assembly '" + id //$NON-NLS-1$ + "' must have an assembler specified since no immediate child has a typeId."); //$NON-NLS-1$ if (id != null) { return new StartupNodeInfo(Level.CUSTOM, sequence, id); } return null; } private String getTypeId(final INode2Extension[] extensions) { if ((extensions != null) && (extensions.length > 0)) { return extensions[0].getNodeId(); } else { return null; } } /** * {@inheritDoc} */ public INavigationNode<?> provideNode(final INavigationNode<?> sourceNode, final NavigationNodeId targetId, final NavigationArgument argument) { return provideNodeHook(sourceNode, targetId, argument); } /** * Returns a navigationNode identified by the given navigationNodeId. The node is created if it not yet exists. * * @param sourceNode * an existing node in the navigation model * @param targetId * the ID of the target node * @param argument * contains information passed used for providing the target node * @param createNodeAsync * @return target node */ @SuppressWarnings("rawtypes") protected INavigationNode<?> provideNodeHook(final INavigationNode<?> sourceNode, final NavigationNodeId targetId, final NavigationArgument argument) { INavigationNode<?> targetNode = findNode(getRootNode(sourceNode), targetId); if (targetNode == null) { if (LOGGER.isLoggable(LogService.LOG_DEBUG)) { LOGGER.log(LogService.LOG_DEBUG, "createNode: " + targetId); //$NON-NLS-1$ } final INavigationAssembler assembler = getNavigationAssembler(targetId, argument); if (assembler != null) { final NavigationNodeId parentTypeId = getParentTypeId(argument, assembler); // ensure parent nodes final INavigationNode parentNode = findNode(provideNodeHook(sourceNode, parentTypeId, null), parentTypeId); INavigationNode<?>[] targetNodes = null; final BuildNodeWrapper buildNodeCallable = new BuildNodeWrapper(assembler, targetId, argument, parentNode); if ((null != argument && argument.isCreateNodesAsync()) || shouldRunAsync(assembler)) { targetNodes = UIExecutor.executeLively(buildNodeCallable); } else { try { targetNodes = buildNodeCallable.call(); } catch (final Exception e) { LOGGER.log(LogService.LOG_ERROR, "Error calling assembler " + assembler + " while building node " + targetId, e); //$NON-NLS-1$ //$NON-NLS-2$ } } if ((targetNodes != null) && (targetNodes.length > 0)) { prepareNodesAfterBuild(targetNodes, argument, parentNode, assembler, targetId, sourceNode); targetNode = targetNodes[0]; } } else { if (getRootNode(sourceNode).isActivated()) { throw new ExtensionPointFailure("No assembler found for ID=" + targetId.getTypeId()); //$NON-NLS-1$ } } } else { prepareExistingNode(sourceNode, targetNode, argument); } if (argument != null) { if (argument.isPrepareAll()) { prepareAll(targetNode); } } // TODO: all targetNodes have to be returned, because overriding subclass may need it. return targetNode; } /** * This {@link Callable} is executed on the same {@link Thread} as {@link INavigationAssembler#buildNode(NavigationNodeId, NavigationArgument)} so that * {@link ThreadLocal}s set in * {@link AbstractSimpleNavigationNodeProvider#prepareNavigationAssembler(NavigationNodeId, INavigationAssembler, INavigationNode)} are available in * {@link INavigationAssembler#buildNode(NavigationNodeId, NavigationArgument)}. */ private class BuildNodeWrapper implements Callable<INavigationNode<?>[]> { private final INavigationAssembler assembler; private final NavigationNodeId targetId; private final NavigationArgument argument; private final INavigationNode<?> parentNode; public BuildNodeWrapper(final INavigationAssembler assembler, final NavigationNodeId targetId, final NavigationArgument argument, final INavigationNode<?> parentNode) { this.assembler = assembler; this.targetId = targetId; this.argument = argument; this.parentNode = parentNode; } public INavigationNode<?>[] call() throws Exception { prepareNavigationAssembler(targetId, assembler, parentNode); return assembler.buildNode(targetId, argument); } } /** * @param assembler * @return <code>true</code> if the given assembler has a {@link RunAsync} annotation, otherwise <code>false</code> */ private boolean shouldRunAsync(final INavigationAssembler assembler) { return assembler.getClass().isAnnotationPresent(RunAsync.class); } /** * @since 3.0 */ protected void prepareNodesAfterBuild(final INavigationNode<?>[] targetNodes, final NavigationArgument argument, final INavigationNode<?> parentNode, final INavigationAssembler assembler, final NavigationNodeId targetId, final INavigationNode<?> sourceNode) { final NodePositioner nodePositioner = argument != null ? argument.getNodePositioner() : NodePositioner.ADD_END; for (final INavigationNode<?> node : targetNodes) { prepareNodeAfterBuild(node, argument, parentNode, assembler, targetId, nodePositioner); } } /** * @since 3.0 */ protected void prepareNodeAfterBuild(final INavigationNode<?> node, final NavigationArgument argument, final INavigationNode<?> parentNode, final INavigationAssembler assembler, final NavigationNodeId targetId, final NodePositioner nodePositioner) { storeNavigationArgument(node, argument); nodePositioner.addChildToParent(parentNode, node); if (node.getNodeId() == null && assembler.getId().equals(targetId.getTypeId())) { node.setNodeId(targetId); } } /** * @since 3.0 */ protected void prepareExistingNode(final INavigationNode<?> sourceNode, final INavigationNode<?> targetNode, final NavigationArgument argument) { storeNavigationArgument(targetNode, argument); } /** * Stores the given argument in the context of the given node * * @param targetNode * target node * @param argument * contains information passed used for providing the target node */ private void storeNavigationArgument(final INavigationNode<?> targetNode, final NavigationArgument argument) { if (argument != null) { targetNode.setContext(NavigationArgument.CONTEXTKEY_ARGUMENT, argument); } } /** * Creates the assembler ({@link INavigationAssembler}) for the given assembly and registers it. * * @param assembly * assembly to register */ public void register(final INavigationAssembly2Extension assembly) { String assemblyId = assembly.getId(); if (assemblyId == null) { if (random == null) { try { random = SecureRandom.getInstance("SHA1PRNG"); //$NON-NLS-1$ } catch (final NoSuchAlgorithmException e) { random = new Random(System.currentTimeMillis()); } } assemblyId = "Riena.random.assemblyid." + Long.valueOf(random.nextLong()).toString(); //$NON-NLS-1$ LOGGER.log(LogService.LOG_DEBUG, "Assembly has no id. Generated a random '" + assemblyId //$NON-NLS-1$ + "'. For Assembler=" + assembly.getNavigationAssembler()); //$NON-NLS-1$ } INavigationAssembler assembler = assembly.createNavigationAssembler(); if (assembler == null) { assembler = createDefaultAssembler(); } assembler.setId(assemblyId); assembler.setParentNodeId(assembly.getParentNodeId()); assembler.setStartOrder(getStartOrder(assembly)); assembler.setAssembly(assembly); registerNavigationAssembler(assemblyId, assembler); } private int getStartOrder(final ICommonNavigationAssemblyExtension assembly) { try { final INavigationAssembly2Extension assembly2 = (INavigationAssembly2Extension) assembly; return assembly2.getStartOrder(); } catch (final NumberFormatException e) { return -1; } } /** * Returns the root node in the navigation model tree for the given node. * * @param node * child node * @return root node */ protected INavigationNode<?> getRootNode(final INavigationNode<?> node) { if (node.getParent() == null) { return node; } return getRootNode(node.getParent()); } /** * Searches for a node that has the given ID and it's a child of the given node. * * @param node * the node form that the search is started * @param targetId * ID of the node that should be found * @return the found node or {@code null} if no matching node was found */ protected INavigationNode<?> findNode(final INavigationNode<?> node, final NavigationNodeId targetId) { if (targetId == null) { return null; } if (targetId.equals(node.getNodeId())) { return node; } for (final INavigationNode<?> child : node.getChildren()) { final INavigationNode<?> foundNode = findNode(child, targetId); if (foundNode != null) { return foundNode; } } return null; } public INavigationAssembler getNavigationAssembler(final NavigationNodeId nodeId, final NavigationArgument argument) { if (nodeId != null && nodeId.getTypeId() != null) { for (final INavigationAssembler probe : getNavigationAssemblers()) { if (probe.acceptsToBuildNode(nodeId, argument)) { return probe; } } } return null; } /** * Returns the ID of the parent node. * * @param argument * contains information passed used for providing the target node. One information can be a parent node * @param assembler * navigation assembler. If the navigation argument has no parent node information the parent node ID of the assembler is returned. * @return ID of the parent node */ private NavigationNodeId getParentTypeId(final NavigationArgument argument, final INavigationAssembler assembler) { if (argument != null && argument.getParentNodeId() != null) { return argument.getParentNodeId(); } else { final String parentTypeId = assembler.getParentNodeId(); if (StringUtils.isEmpty(parentTypeId)) { final String id = assembler.getId(); throw new ExtensionPointFailure("parentTypeId cannot be null or blank for assembly ID=" //$NON-NLS-1$ + id); } return new NavigationNodeId(parentTypeId); } } /** * Prepares the given node and all its children. * * @param node * navigation node to prepare. */ private void prepareAll(final INavigationNode<?> node) { node.prepare(); for (final INavigationNode<?> child : node.getChildren()) { prepareAll(child); } } /** * {@inheritDoc} */ public void cleanUp() { assemblyId2AssemblerCache.clear(); } /** * {@inheritDoc} */ public INavigationAssembler getNavigationAssembler(final String assemblyId) { return assemblyId2AssemblerCache.get(assemblyId); } /** * Returns all registered assemblers. * * @return collection of all assemblers */ public Collection<INavigationAssembler> getNavigationAssemblers() { return assemblyId2AssemblerCache.values(); } /** * Registers the given assembler and associates it with the given assembly ID. * <p> * If another assembler is already registered with the same ID an exception ({@link IllegalStateException}) is thrown. * * @param id * ID of the assembly * @param assembler * assembler to register */ public void registerNavigationAssembler(final String id, final INavigationAssembler assembler) { final INavigationAssembler oldAssembler = assemblyId2AssemblerCache.put(id, assembler); if (oldAssembler != null) { final String msg = String.format("There are two assembly extension definitions for '%s'.", id); //$NON-NLS-1$ final RuntimeException runtimeExc = new IllegalStateException(msg); LOGGER.log(LogService.LOG_ERROR, msg, runtimeExc); throw runtimeExc; } } /** * Used to prepare the assembler in a application specific way. * * @param targetId * @param assembler * @param parentNode */ protected void prepareNavigationAssembler(final NavigationNodeId targetId, final INavigationAssembler assembler, final INavigationNode<?> parentNode) { if (assembler instanceof IGenericNavigationAssembler) { ((IGenericNavigationAssembler) assembler).setAssemblerProvider(this); } } /** * Creates a generic assembler that is used if the assembly has no assembler. * * @return default assembler */ protected INavigationAssembler createDefaultAssembler() { return new GenericNavigationAssembler(); } }