/* * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.flex.compiler.internal.codegen.databinding; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.WeakHashMap; import org.apache.flex.compiler.definitions.IClassDefinition; import org.apache.flex.compiler.internal.as.codegen.MXMLClassDirectiveProcessor; import org.apache.flex.compiler.internal.codegen.databinding.WatcherInfoBase.WatcherType; import org.apache.flex.compiler.internal.scopes.ASScope; import org.apache.flex.compiler.problems.ICompilerProblem; import org.apache.flex.compiler.tree.as.IASNode; import org.apache.flex.compiler.tree.mxml.IMXMLBindingNode; import org.apache.flex.compiler.tree.mxml.IMXMLDataBindingNode; /** * After all the bindings in an MXML document (class) are analyzed, this * database will have all the information required to CG the getters, bindings, and watchers * * Note that part of the division of labor is that that the database and the analyzers is uses to get * its data know about parse tree nodes, but clients of the database should not need to know about nodes. * * Conversely the database does not know anything about ABC, but presumably the client of the database does. * * Clients are assumed to use the database as follows: * create a single database for an MXML document (class) * call analyze() on each binding node found in the document. * call finishAnalysis(). * * Then call various getXXX functions to retrieve information */ public class BindingDatabase { public BindingDatabase() { // Testing only: If someone has registered interest, send them "this" if (_diagnosticLogger != null) { _diagnosticLogger.add(this); } } /************** private data ****************/ private Set<BindingInfo> bindingInfoSet = new TreeSet<BindingInfo>(); /** * map that relates one binding index to another * If the pair is in this map, then there is a two way binding relationship between them * Note that key/value makes no difference... Either side of the relationship can be the Key or the Value, * but a pair must only be represented once */ private Map<Integer, Integer> twoWayPairs = null; // Note that we are using a single WatcherInfo as a holder // for all the watchers, which are its children. // This isn't ideal, but the alternative had a lot of code duplication, // which was worse. private WatcherInfoBase watcherList = new WatcherInfoBase(null, null, new ArrayList<String>()); private int numWatchers=-1; private ASScope scope; private boolean analysisFinished = false; private static List<BindingDatabase> _diagnosticLogger; public static WeakHashMap<IClassDefinition, BindingDatabase> bindingMap = new WeakHashMap<IClassDefinition, BindingDatabase>(); /** * test only field. Total number of watcher info's of all types */ public int _watchersCreated; /**************************** public functions ******************/ /** * Unit tests may use this API to get notified of Database creation * Here are the rules: * Don't call this from production code - just for unit testsing * Always call later with null to remove the logger */ public static void _setDiagnosticLogger(List<BindingDatabase> logger) { assert ((_diagnosticLogger == null) || (logger==null)); _diagnosticLogger = logger; } /** * Will do the binding analysis on a single binding node. * May find bindings and watchers already marked for creation by other nodes and re-use them. * */ public BindingInfo analyze(IMXMLDataBindingNode node, Collection<ICompilerProblem> problems, MXMLClassDirectiveProcessor host) { BindingInfo bindingInfo = BindingAnalyzer.analyze(node, this, problems, this.bindingInfoSet.size(), host); this.bindingInfoSet.add(bindingInfo); // now watchers WatcherAnalyzer watcherAnalyzer = new WatcherAnalyzer(this, problems, bindingInfo, host.getProject()); watcherAnalyzer.analyze(); return bindingInfo; } /** * Will do the binding analysis on a single binding node. * May find bindings and watchers already marked for creation by other nodes and re-use them. * */ public void analyzeBindingNode(IMXMLBindingNode node, Collection<ICompilerProblem> problems, MXMLClassDirectiveProcessor host) { BindingInfo info1 = analyzeBindingNode(node, problems, host, false); if (node.getTwoWay()) { // If two way nodex, then analyze again, reversing source and destination roles BindingInfo info2 = analyzeBindingNode(node, problems, host, true); // And note the two bindings are "paired" info1.setTwoWayCounterpart( info2.getIndex()); info2.setTwoWayCounterpart( info1.getIndex()); } } private BindingInfo analyzeBindingNode(IMXMLBindingNode node, Collection<ICompilerProblem> problems, MXMLClassDirectiveProcessor host, boolean reverseSourceAndDest) { BindingInfo bindingInfo = BindingAnalyzer.analyze(node, this, problems, this.bindingInfoSet.size(), reverseSourceAndDest, host); this.bindingInfoSet.add(bindingInfo); // now watchers WatcherAnalyzer watcherAnalyzer = new WatcherAnalyzer(this, problems, bindingInfo, host.getProject()); watcherAnalyzer.analyze(); return bindingInfo; } /** * Must be called once after all the nodes in a document have been analyzed */ public void finishAnalysis() { assert !analysisFinished; assignWatchersToSlots(); makeTwoWayMap(); analysisFinished = true; assert isValid(); } /** * @return information on all the bindings. * * Note that the set will be sorted in the order the binding were created, * which is their "slot index" */ public Set<BindingInfo> getBindingInfo() { return bindingInfoSet; } /** * returns a map relating BindingInfos that are paired in two way bindings * If a binding is returns as a key, it will also not be found as a value - each * binding only appears once * */ public Map<Integer, Integer> getTwoWayBindingInfoPairs() { assert twoWayPairs != null; return twoWayPairs; } /** * @return information about all the watchers - or null if none * * For efficiency reasons, we just return the entry set. We could be nice and turn it into * a list, but the overhead isn't worth if for a low level function like this */ public Set< Entry<Object, WatcherInfoBase>> getWatcherChains() { assert analysisFinished; return watcherList.getChildren(); } /** * * @return total number of watchers required for document */ public int getNumWatchers() { assert analysisFinished; return numWatchers; } /** * @return true is class requires a propertyGetter function */ public boolean getRequiresPropertyGetter() { assert analysisFinished; boolean ret = false; Set<Entry<Object, WatcherInfoBase>> rootWatchers = getWatcherChains(); if (rootWatchers == null) return false; for (Entry<Object, WatcherInfoBase> rootWatcherEntry : rootWatchers) { WatcherInfoBase rootWatcher = rootWatcherEntry.getValue(); if (rootWatcher.type == WatcherType.PROPERTY) { ret = true; // we found a non-static property watcher at the root of a chain } } return ret; } /** * returns the containing scope of one of the databinding nodes that have been analyzed */ public ASScope getScope() { return scope; } public void setScope(ASScope scope) { assert(this.scope==null || this.scope == scope); this.scope = scope; } /** * Searches for appropriate existing watcher, or creates new one. * * If new one is created, it will be added to the correct place in the watcher chains * * @param parent - the parent watcher of the watcher we are interested in, or null for root watchers * @param type - type type of watcher we want to find/create * @param watcherKey - the object that identifies this watcher (map key). Often it is the definition, but we * don't always have a definition for dynamic properties * @param problems - if semantic problems are found, report them here. * @param sourceNode - parse node we are basing the watcher on * @param eventNames - a list of events that the watcher must listen for */ public WatcherInfoBase getOrCreateWatcher( WatcherInfoBase parent, WatcherInfoBase.WatcherType type, Object watcherKey, Collection<ICompilerProblem> problems, IASNode sourceNode, List<String> eventNames ) { assert watcherKey!= null; // or is it OK? WatcherInfoBase ret = null; // final watcher will either be under "parent", or will be under the root, // depending on args passed in WatcherInfoBase newParent = (parent != null) ? parent : watcherList; // look to see if the desired one already exists WatcherInfoBase existingChild = newParent.getOrCreateChildren().get(watcherKey); if (existingChild != null) { ret = existingChild; } else { switch (type) { case PROPERTY: ret = new PropertyWatcherInfo(problems, sourceNode, eventNames); break; case STATIC_PROPERTY: ret = new StaticPropertyWatcherInfo(problems, sourceNode, eventNames); break; case FUNCTION: ret = new FunctionWatcherInfo(problems, sourceNode, eventNames); break; case XML: ret = new XMLWatcherInfo(problems, sourceNode, eventNames); break; default: assert false; break; } ret.isRoot = (parent == null); newParent.children.put(watcherKey, ret); } // make sure if we re-use an existing one that it is the correct type assert objIsType(ret, type); return ret; } private boolean objIsType(WatcherInfoBase obj, WatcherType type) { boolean ret = false; switch (type) { case PROPERTY: ret = obj instanceof PropertyWatcherInfo; break; case STATIC_PROPERTY: ret = obj instanceof StaticPropertyWatcherInfo; break; case FUNCTION: ret = obj instanceof FunctionWatcherInfo; break; case XML: ret = obj instanceof XMLWatcherInfo; break; } return ret; } /** * just for debugging */ @Override public String toString() { StringBuilder sb = new StringBuilder(); if (bindingInfoSet.isEmpty()) { sb.append("<no binding info"); } else { sb.append("bindingInfo:\n"); boolean first = true; for (BindingInfo info : bindingInfoSet) { if (!first) sb.append("\n"); sb.append(" " + info ); first = false; } } if (watcherList.getChildren() != null) { sb.append("\nwatcher chains: "); for (Entry<Object, WatcherInfoBase> ent : watcherList.getChildren()) { sb.append("\n" + ent.getValue().dump(" ")); } } else { sb.append("\n<no watcher chains>"); } return sb.toString(); } /*************************** private methods ************************/ private void assignWatchersToSlots() { int curIndex = -1; curIndex = watcherList.assignIndex(curIndex); numWatchers = curIndex; } /** * Find all the two way binding relationships, and put them into this.twoWayPairs */ private void makeTwoWayMap() { assert twoWayPairs == null; twoWayPairs = new LinkedHashMap<Integer, Integer>(); for (BindingInfo bindingInfo : this.getBindingInfo()) { if (bindingInfo.getTwoWayCounterpart() >= 0) // if this one part of a two way expression { if ((twoWayPairs.get(bindingInfo.getIndex())==null) && (twoWayPairs.get(bindingInfo.getTwoWayCounterpart()))==null) { twoWayPairs.put(bindingInfo.getIndex(), bindingInfo.getTwoWayCounterpart()); } } } } // just for debugging private boolean isValid() { int i=0; for (BindingInfo info : bindingInfoSet) { if (info.getIndex() != i) return false; ++i; } return true; } }