| /* |
| * Copyright 2009 Google Inc. |
| * |
| * 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 com.google.gwt.core.ext.soyc.impl; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.core.ext.soyc.ClassMember; |
| import com.google.gwt.core.ext.soyc.FunctionMember; |
| import com.google.gwt.core.ext.soyc.Member; |
| import com.google.gwt.core.ext.soyc.Range; |
| import com.google.gwt.core.ext.soyc.Story; |
| import com.google.gwt.core.ext.soyc.Story.Origin; |
| import com.google.gwt.dev.jjs.Correlation; |
| import com.google.gwt.dev.jjs.SourceInfo; |
| import com.google.gwt.dev.jjs.Correlation.Axis; |
| import com.google.gwt.dev.jjs.ast.JDeclaredType; |
| import com.google.gwt.dev.jjs.ast.JField; |
| import com.google.gwt.dev.jjs.ast.JMethod; |
| import com.google.gwt.dev.js.ast.JsFunction; |
| import com.google.gwt.dev.util.Util; |
| import com.google.gwt.util.tools.Utility; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.Stack; |
| import java.util.TreeSet; |
| import java.util.zip.GZIPOutputStream; |
| |
| /** |
| * Records {@link Story}s to a file for Compile Reports. |
| */ |
| public class StoryRecorder { |
| |
| /** |
| * Associates a SourceInfo with a Range. |
| */ |
| private static class RangeInfo { |
| public final SourceInfo info; |
| public final Range range; |
| |
| public RangeInfo(Range range, SourceInfo info) { |
| assert range != null; |
| assert info != null; |
| this.range = range; |
| this.info = info; |
| } |
| } |
| |
| private static final int MAX_STRING_BUILDER_SIZE = 65536; |
| |
| /** |
| * Used to record dependencies of a program. |
| */ |
| public static void recordStories(TreeLogger logger, OutputStream out, |
| List<Map<Range, SourceInfo>> sourceInfoMaps, String[] js) { |
| new StoryRecorder().recordStoriesImpl(logger, out, sourceInfoMaps, js); |
| } |
| |
| private StringBuilder builder; |
| |
| private int curHighestFragment = 0; |
| |
| private OutputStream gzipStream; |
| |
| private String[] js; |
| |
| /** |
| * Used by {@link #popAndRecord(Stack)} to determine start and end ranges. |
| */ |
| private int lastEnd = 0; |
| |
| /** |
| * This is a class field for convenience, but it should be deleted at the end |
| * of the constructor. |
| */ |
| private transient Map<Correlation, Member> membersByCorrelation = new IdentityHashMap<Correlation, Member>(); |
| |
| /** |
| * This is a class field for convenience, but it should be deleted at the end |
| * of the constructor. |
| */ |
| private transient Map<SourceInfo, StoryImpl> storyCache = new IdentityHashMap<SourceInfo, StoryImpl>(); |
| private Map<Story, Integer> storyIds = new HashMap<Story, Integer>(); |
| |
| private StoryRecorder() { |
| } |
| |
| protected void recordStoriesImpl(TreeLogger logger, OutputStream out, |
| List<Map<Range, SourceInfo>> sourceInfoMaps, String[] js) { |
| |
| logger = logger.branch(TreeLogger.INFO, "Creating Stories file for the compile report"); |
| |
| this.js = js; |
| |
| try { |
| builder = new StringBuilder(MAX_STRING_BUILDER_SIZE * 2); |
| gzipStream = new GZIPOutputStream(out); |
| |
| /* |
| * Don't retain beyond the constructor to avoid lingering references to |
| * AST nodes. |
| */ |
| MemberFactory memberFactory = new MemberFactory(); |
| |
| // Record what we've seen so far |
| TreeSet<ClassMember> classesMutable = new TreeSet<ClassMember>( |
| Member.SOURCE_NAME_COMPARATOR); |
| TreeSet<FunctionMember> functionsMutable = new TreeSet<FunctionMember>( |
| Member.SOURCE_NAME_COMPARATOR); |
| Set<SourceInfo> sourceInfoSeen = new HashSet<SourceInfo>(); |
| |
| builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soyc>\n<stories>\n"); |
| |
| int fragment = 0; |
| for (Map<Range, SourceInfo> sourceInfoMap : sourceInfoMaps) { |
| lastEnd = 0; |
| analyzeFragment(memberFactory, classesMutable, functionsMutable, |
| sourceInfoMap, sourceInfoSeen, fragment++); |
| |
| // Flush output to improve memory locality |
| flushOutput(); |
| } |
| |
| builder.append("</stories>\n</soyc>\n"); |
| |
| /* |
| * Clear the member fields that we don't need anymore to allow GC of the |
| * SourceInfo objects |
| */ |
| membersByCorrelation = null; |
| storyCache = null; |
| |
| Util.writeUtf8(builder, gzipStream); |
| Utility.close(gzipStream); |
| |
| logger.log(TreeLogger.INFO, "Done"); |
| } catch (Throwable e) { |
| logger.log(TreeLogger.ERROR, "Could not write dependency file.", e); |
| } |
| } |
| |
| private void analyzeFragment(MemberFactory memberFactory, |
| TreeSet<ClassMember> classesMutable, |
| TreeSet<FunctionMember> functionsMutable, |
| Map<Range, SourceInfo> sourceInfoMap, Set<SourceInfo> sourceInfoSeen, |
| int fragment) throws IOException { |
| /* |
| * We want to iterate over the Ranges so that enclosing Ranges come before |
| * their enclosed Ranges... |
| */ |
| Range[] dependencyOrder = sourceInfoMap.keySet().toArray( |
| new Range[sourceInfoMap.size()]); |
| Arrays.sort(dependencyOrder, Range.DEPENDENCY_ORDER_COMPARATOR); |
| |
| Stack<RangeInfo> dependencyScope = new Stack<RangeInfo>(); |
| for (Range range : dependencyOrder) { |
| SourceInfo info = sourceInfoMap.get(range); |
| assert info != null; |
| |
| // Infer dependency information |
| if (!dependencyScope.isEmpty()) { |
| |
| /* |
| * Pop frames until we get back to a container, using this as a chance |
| * to build up our list of non-overlapping Ranges to report back to the |
| * user. |
| */ |
| while (!dependencyScope.peek().range.contains(range)) { |
| popAndRecord(dependencyScope, fragment); |
| } |
| } |
| |
| // Possibly create and record Members |
| if (!sourceInfoSeen.contains(info)) { |
| sourceInfoSeen.add(info); |
| for (Correlation c : info.getPrimaryCorrelationsArray()) { |
| if (c == null) { |
| continue; |
| } |
| if (membersByCorrelation.containsKey(c)) { |
| continue; |
| } |
| |
| switch (c.getAxis()) { |
| case CLASS: { |
| JDeclaredType type = c.getType(); |
| StandardClassMember member = memberFactory.get(type); |
| membersByCorrelation.put(c, member); |
| classesMutable.add(member); |
| break; |
| } |
| case FIELD: { |
| JField field = c.getField(); |
| JDeclaredType type = c.getType(); |
| StandardFieldMember member = memberFactory.get(field); |
| memberFactory.get(type).addField(member); |
| membersByCorrelation.put(c, member); |
| break; |
| } |
| case FUNCTION: { |
| JsFunction function = c.getFunction(); |
| StandardFunctionMember member = memberFactory.get(function); |
| membersByCorrelation.put(c, member); |
| functionsMutable.add(member); |
| break; |
| } |
| case METHOD: { |
| JMethod method = c.getMethod(); |
| JDeclaredType type = c.getType(); |
| StandardMethodMember member = memberFactory.get(method); |
| memberFactory.get(type).addMethod(member); |
| membersByCorrelation.put(c, member); |
| break; |
| } |
| } |
| } |
| } |
| |
| dependencyScope.push(new RangeInfo(range, info)); |
| } |
| |
| // Unwind the rest of the stack to finish out the ranges |
| while (!dependencyScope.isEmpty()) { |
| popAndRecord(dependencyScope, fragment); |
| } |
| |
| /* |
| * Because the first Range corresponds to the SourceInfo of the whole |
| * program, we'll know that we got all of the data if the ends match up. If |
| * this assert passes, we know that we've correctly generated a sequence of |
| * non-overlapping Ranges that encompass the whole program. |
| */ |
| assert dependencyOrder[0].getEnd() == lastEnd; |
| } |
| |
| private void emitStory(StoryImpl story, Range range) throws IOException { |
| int storyNum; |
| if (storyIds.containsKey(story)) { |
| storyNum = storyIds.get(story); |
| } else { |
| storyNum = storyIds.size(); |
| storyIds.put(story, storyNum); |
| } |
| |
| builder.append("<story id=\"story"); |
| builder.append(storyNum); |
| if (story.getLiteralTypeName() != null) { |
| builder.append("\" literal=\""); |
| builder.append(story.getLiteralTypeName()); |
| } |
| builder.append("\">\n"); |
| |
| Set<Origin> origins = story.getSourceOrigin(); |
| if (origins.size() > 0) { |
| builder.append("<origins>\n"); |
| for (Origin origin : origins) { |
| builder.append("<origin lineNumber=\""); |
| builder.append(origin.getLineNumber()); |
| builder.append("\" location=\""); |
| builder.append(origin.getLocation()); |
| builder.append("\"/>\n"); |
| |
| flushOutput(); |
| } |
| builder.append("</origins>\n"); |
| } |
| |
| Set<Member> correlations = story.getMembers(); |
| if (correlations.size() > 0) { |
| builder.append("<correlations>\n"); |
| for (Member correlation : correlations) { |
| builder.append("<by idref=\""); |
| builder.append(correlation.getSourceName()); |
| builder.append("\"/>\n"); |
| |
| flushOutput(); |
| } |
| builder.append("</correlations>\n"); |
| } |
| |
| builder.append("<js fragment=\""); |
| builder.append(curHighestFragment); |
| builder.append("\"/>\n<storyref idref=\"story"); |
| builder.append(storyNum); |
| |
| int start = range.getStart(); |
| int end = range.getEnd(); |
| String jsCode = js[curHighestFragment]; |
| if ((start == end) || ((end == start + 1) && jsCode.charAt(start) == '\n')) { |
| builder.append("\"/>\n</story>\n"); |
| } else { |
| builder.append("\">"); |
| SizeMapRecorder.escapeXml(jsCode, start, end, false, builder); |
| builder.append("</storyref>\n</story>\n"); |
| } |
| } |
| |
| private void flushOutput() throws IOException { |
| // Flush output to improve memory locality |
| if (builder.length() > MAX_STRING_BUILDER_SIZE) { |
| Util.writeUtf8(builder, gzipStream); |
| builder.setLength(0); |
| } |
| } |
| |
| /** |
| * Remove an element from the RangeInfo stack and stare a new StoryImpl with |
| * the right length, possibly sub-dividing the super-enclosing Range in the |
| * process. |
| */ |
| private void popAndRecord(Stack<RangeInfo> dependencyScope, int fragment) |
| throws IOException { |
| RangeInfo rangeInfo = dependencyScope.pop(); |
| Range toStore = rangeInfo.range; |
| |
| /* |
| * Make a new Range for the gap between the popped Range and whatever we |
| * last stored. |
| */ |
| if (lastEnd < toStore.getStart()) { |
| Range newRange = new Range(lastEnd, toStore.getStart()); |
| assert !dependencyScope.isEmpty(); |
| |
| SourceInfo gapInfo = dependencyScope.peek().info; |
| recordStory(gapInfo, fragment, newRange.length(), newRange); |
| |
| lastEnd += newRange.length(); |
| } |
| |
| /* |
| * Store as much of the current Range as we haven't previously stored. The |
| * Max.max() is there to take care of the tail end of Ranges that have had a |
| * sub-range previously stored. |
| */ |
| if (lastEnd < toStore.getEnd()) { |
| Range newRange = new Range(Math.max(lastEnd, toStore.getStart()), |
| toStore.getEnd()); |
| recordStory(rangeInfo.info, fragment, newRange.length(), newRange); |
| lastEnd += newRange.length(); |
| } |
| } |
| |
| private void recordStory(SourceInfo info, int fragment, int length, |
| Range range) throws IOException { |
| assert info != null; |
| assert storyCache != null; |
| |
| if (fragment > curHighestFragment) { |
| curHighestFragment = fragment; |
| } |
| |
| StoryImpl theStory; |
| if (!storyCache.containsKey(info)) { |
| SortedSet<Member> members = new TreeSet<Member>( |
| Member.TYPE_AND_SOURCE_NAME_COMPARATOR); |
| Set<Origin> origins = new HashSet<Origin>(); |
| |
| for (Correlation c : info.getAllCorrelations()) { |
| Member m = membersByCorrelation.get(c); |
| if (m != null) { |
| members.add(m); |
| } |
| |
| if (c.getAxis() == Axis.ORIGIN) { |
| origins.add(new OriginImpl(c.getOrigin())); |
| } |
| } |
| |
| String literalType = null; |
| Correlation literalCorrelation = info.getPrimaryCorrelation(Axis.LITERAL); |
| if (literalCorrelation != null) { |
| literalType = literalCorrelation.getLiteral().getDescription(); |
| } |
| |
| theStory = new StoryImpl(storyCache.size(), members, origins, |
| literalType, fragment, length); |
| storyCache.put(info, theStory); |
| } else { |
| // Use a copy-constructed instance |
| theStory = new StoryImpl(storyCache.get(info), length); |
| } |
| |
| emitStory(theStory, range); |
| } |
| |
| } |