/********************************************************************* Copyright 2014 the Flapi authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ********************************************************************/ package unquietcode.tools.flapi.graph; import unquietcode.tools.flapi.DescriptorBuilderException; import unquietcode.tools.flapi.graph.components.*; import unquietcode.tools.flapi.outline.*; import java.util.*; import static com.google.common.base.Preconditions.checkState; /** * @author Ben Fagin * @version 08-12-2012 */ public class GraphBuilder { private Map<String, StateClass> blocks = new HashMap<String, StateClass>(); private Map<String, StateClass> states = new HashMap<String, StateClass>(); private Map<BlockReference, BlockOutline> referenceMap = new IdentityHashMap<BlockReference, BlockOutline>(); public StateClass buildGraph(DescriptorOutline descriptor) { // resolve block references Map<String, BlockOutline> blocks = BlockOutline.findAllBlocks(descriptor); initializeReferenceMap(blocks, referenceMap, descriptor); return convertBlock(descriptor); } private static void initializeReferenceMap( Map<String, BlockOutline> blocks, Map<BlockReference, BlockOutline> references, BlockOutline block ){ if (block instanceof BlockReference) { final BlockReference reference = (BlockReference) block; final BlockOutline resolved; // check for a direct reference first if (reference.directReference() != null) { resolved = reference.directReference(); } // then check for it by name else { resolved = blocks.get(reference.getName()); } // it wasn't a direct reference, and we couldn't find it by name if (resolved == null) { throw new DescriptorBuilderException("Could not resolve block reference with name '"+block.getName()+"'."); } references.put((BlockReference) block, resolved); } for (MethodOutline method : block.getAllMethods()) { for (BlockOutline chain : method.getBlockChain()) { initializeReferenceMap(blocks, references, chain); } } } private StateClass convertBlock(BlockOutline block) { StateClass topLevel; String blockName = block.getName(); Set<MethodOutline> triggeredMethods = block.getTriggeredMethods(); Set<MethodOutline> allMethods = block.getAllMethods(); allMethods.removeAll(triggeredMethods); if (blocks.containsKey(block.getName())) { return blocks.get(block.getName()); } else if (block instanceof BlockReference) { BlockOutline resolved = referenceMap.get(block); return convertBlock(resolved); } else { topLevel = getStateFromBlockAndMethods(block, allMethods); topLevel.setIsTopLevel(); blocks.put(blockName, topLevel); } // marks all states as being related to each other via // a shared reference to this marker object final Object baseIdentifier = new Object(); // create the sibling states Set<StateClass> seen = Collections.newSetFromMap(new IdentityHashMap<StateClass, Boolean>()); Set<Set<MethodOutline>> workingSet = new HashSet<Set<MethodOutline>>(); workingSet.add(new TreeSet<>(allMethods)); while (!workingSet.isEmpty()) { Set<Set<MethodOutline>> nextSet = new HashSet<Set<MethodOutline>>(); for (Set<MethodOutline> combination : workingSet) { StateClass theState = getStateFromBlockAndMethods(block, combination); if (seen.contains(theState)) { continue; } else { seen.add(theState); } theState.setName(blockName); theState.setBlockMarker(baseIdentifier); // add dynamic methods for (MethodOutline method : combination) { Set<MethodOutline> next; next = addTransition(theState, block, combination, triggeredMethods, method); nextSet.add(next); } } workingSet = nextSet; } for (BlockOutline child : block.getBlocks()) { convertBlock(child); } return topLevel; } private StateClass getStateFromBlockAndMethods(BlockOutline block, Set<MethodOutline> allMethods) { StringBuilder sb = new StringBuilder(); // block name sb.append(block.getName()); TreeSet<String> names = new TreeSet<String>(); for (MethodOutline method : allMethods) { names.add(method.keyString()+"-"+method.getMaxOccurrences()); } // method names for (String name : names) { sb.append(name); } String key = sb.toString(); if (states.containsKey(key)) { return states.get(key); } StateClass state = new StateClass(block.getHelperClass(), block.getBeanClass()); state.setName(block.getName()); states.put(key, state); return state; } private Set<MethodOutline> addTransition( StateClass state, BlockOutline block, Set<MethodOutline> combination, Set<MethodOutline> triggered, MethodOutline method ){ final Set<MethodOutline> nextMethods = computeNextMethods(combination, triggered, method); final StateClass next = getStateFromBlockAndMethods(block, nextMethods); final Transition transition; if (method.isTerminal()) { if (method.getReturnType() != null) { TerminalTransition terminal = new TerminalTransition(); terminal.setReturnType(method.getReturnType()); transition = terminal; } else if (block.getReturnType() != null) { TerminalTransition terminal = new TerminalTransition(); terminal.setReturnType(block.getReturnType()); transition = terminal; } else { checkState(method.isRequired()); // all terminal methods should be required transition = new AscendingTransition(true); } } else if (state == next) { // as in, "no changes detected" transition = new RecursiveTransition(); } else if (nextMethods.isEmpty()) { transition = new AscendingTransition(method.isRequired()); } else { LateralTransition lateral = new LateralTransition(); lateral.setSibling(next); transition = lateral; } transition.setMethodInfo(((MethodInfo) method).copy()); transition.getChainParameterPositions().addAll(method.getChainParameterPositions()); state.addTransitions(transition); // state chain for (BlockOutline chain : method.getBlockChain()) { StateClass chainClass = convertBlock(chain); transition.getStateChain().add(chainClass); } return nextMethods; } /* Computes the set of next methods. First decrements the method and removes it if dynamic (minus method). Then adds any triggered methods. */ private Set<MethodOutline> computeNextMethods( Set<MethodOutline> allMethods, Set<MethodOutline> triggeredMethods, MethodOutline method ){ // nothing for terminals if (method.isTerminal()) { return new TreeSet<MethodOutline>(); } // compute minus method Set<MethodOutline> nextMethods = new TreeSet<MethodOutline>(allMethods); nextMethods.remove(method); final MethodOutline next; // stays removed if it's the last instance if (method.getMaxOccurrences() == 1) { next = null; } // it must be required in some way, but // we need to make sure it doesn't 'retrigger' else if (method.getMaxOccurrences() < 1) { MethodOutline copy = method.copy(); nextMethods.add(copy); next = copy; } // only add back if it's not the last instance else { /* if (method.getMaxOccurrences() > 1) */ MethodOutline m = method.copy(); m.setMaxOccurrences(m.getMaxOccurrences() - 1); nextMethods.add(m); next = m; } // make changes based on the outgoing group number Integer currentGroup = method.getGroup(); if (currentGroup != null) { for (MethodOutline otherMethod : new TreeSet<MethodOutline>(nextMethods)) { // don't remove ourselves! if (otherMethod == next) { continue; } // remove methods linked by group if (currentGroup.equals(otherMethod.getGroup())) { nextMethods.remove(otherMethod); } } for (MethodOutline triggeredMethod : triggeredMethods) { // add methods triggered by group if (currentGroup.equals(triggeredMethod.getTrigger())) { if (next != null) { next.setTriggered(); } // add the trigger to the next group if (!method.didTrigger()) { nextMethods.add(triggeredMethod.copy()); } } } } // we might be able to switch out an implicit terminal if (nextMethods.size() == 1) { MethodOutline nextMethod = nextMethods.iterator().next(); if (!nextMethod.isTerminal() && !nextMethod.isRequired()) { MethodOutline copy = nextMethod.copy(); copy.isImplicit(true); copy.isTerminal(true); nextMethods = new TreeSet<MethodOutline>(); nextMethods.add(copy); } } return nextMethods; } }