/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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.dart.engine.internal.builder;
import com.google.dart.engine.AnalysisEngine;
import com.google.dart.engine.context.AnalysisException;
import com.google.dart.engine.element.HtmlScriptElement;
import com.google.dart.engine.error.AnalysisError;
import com.google.dart.engine.error.ErrorCode;
import com.google.dart.engine.error.HtmlWarningCode;
import com.google.dart.engine.html.ast.HtmlScriptTagNode;
import com.google.dart.engine.html.ast.HtmlUnit;
import com.google.dart.engine.html.ast.XmlAttributeNode;
import com.google.dart.engine.html.ast.XmlTagNode;
import com.google.dart.engine.html.ast.visitor.XmlVisitor;
import com.google.dart.engine.html.scanner.TokenType;
import com.google.dart.engine.internal.context.InternalAnalysisContext;
import com.google.dart.engine.internal.context.RecordingErrorListener;
import com.google.dart.engine.internal.element.EmbeddedHtmlScriptElementImpl;
import com.google.dart.engine.internal.element.ExternalHtmlScriptElementImpl;
import com.google.dart.engine.internal.element.HtmlElementImpl;
import com.google.dart.engine.internal.element.LibraryElementImpl;
import com.google.dart.engine.internal.resolver.Library;
import com.google.dart.engine.internal.resolver.LibraryResolver;
import com.google.dart.engine.source.Source;
import com.google.dart.engine.utilities.io.UriUtilities;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* Instances of the class {@code HtmlUnitBuilder} build an element model for a single HTML unit.
*/
public class HtmlUnitBuilder implements XmlVisitor<Void> {
private static final String SRC = "src";
/**
* The analysis context in which the element model will be built.
*/
private final InternalAnalysisContext context;
/**
* The error listener to which errors will be reported.
*/
private RecordingErrorListener errorListener;
/**
* The modification time of the source for which an element is being built.
*/
private long modificationStamp;
/**
* The HTML element being built.
*/
private HtmlElementImpl htmlElement;
/**
* The elements in the path from the HTML unit to the current tag node.
*/
private ArrayList<XmlTagNode> parentNodes;
/**
* The script elements being built.
*/
private ArrayList<HtmlScriptElement> scripts;
/**
* A set of the libraries that were resolved while resolving the HTML unit.
*/
private Set<Library> resolvedLibraries = new HashSet<Library>();
/**
* Initialize a newly created HTML unit builder.
*
* @param context the analysis context in which the element model will be built
*/
public HtmlUnitBuilder(InternalAnalysisContext context) {
this.context = context;
this.errorListener = new RecordingErrorListener();
}
/**
* Build the HTML element for the given source.
*
* @param source the source describing the compilation unit
* @param modificationStamp the modification time of the source for which an element is being
* built
* @param unit the AST structure representing the HTML
* @throws AnalysisException if the analysis could not be performed
*/
public HtmlElementImpl buildHtmlElement(Source source, long modificationStamp, HtmlUnit unit)
throws AnalysisException {
this.modificationStamp = modificationStamp;
HtmlElementImpl result = new HtmlElementImpl(context, source.getShortName());
result.setSource(source);
htmlElement = result;
unit.accept(this);
htmlElement = null;
unit.setElement(result);
return result;
}
/**
* Return the listener to which analysis errors will be reported.
*
* @return the listener to which analysis errors will be reported
*/
public RecordingErrorListener getErrorListener() {
return errorListener;
}
/**
* Return an array containing information about all of the libraries that were resolved.
*
* @return an array containing the libraries that were resolved
*/
public Set<Library> getResolvedLibraries() {
return resolvedLibraries;
}
@Override
public Void visitHtmlScriptTagNode(HtmlScriptTagNode node) {
if (parentNodes.contains(node)) {
return reportCircularity(node);
}
parentNodes.add(node);
try {
Source htmlSource = htmlElement.getSource();
XmlAttributeNode scriptAttribute = getScriptSourcePath(node);
String scriptSourcePath = scriptAttribute == null ? null : scriptAttribute.getText();
if (node.getAttributeEnd().getType() == TokenType.GT && scriptSourcePath == null) {
EmbeddedHtmlScriptElementImpl script = new EmbeddedHtmlScriptElementImpl(node);
try {
LibraryResolver resolver = new LibraryResolver(context);
LibraryElementImpl library = (LibraryElementImpl) resolver.resolveEmbeddedLibrary(
htmlSource,
modificationStamp,
node.getScript(),
true);
script.setScriptLibrary(library);
resolvedLibraries.addAll(resolver.getResolvedLibraries());
errorListener.addAll(resolver.getErrorListener());
} catch (AnalysisException exception) {
//TODO (danrubel): Handle or forward the exception
AnalysisEngine.getInstance().getLogger().logError(
"Could not resolve script tag",
exception);
}
node.setScriptElement(script);
scripts.add(script);
} else {
ExternalHtmlScriptElementImpl script = new ExternalHtmlScriptElementImpl(node);
if (scriptSourcePath != null) {
try {
scriptSourcePath = UriUtilities.encode(scriptSourcePath);
// Force an exception to be thrown if the URI is invalid so that we can report the
// problem.
new URI(scriptSourcePath);
Source scriptSource = context.getSourceFactory().resolveUri(
htmlSource,
scriptSourcePath);
script.setScriptSource(scriptSource);
if (!context.exists(scriptSource)) {
reportValueError(
HtmlWarningCode.URI_DOES_NOT_EXIST,
scriptAttribute,
scriptSourcePath);
}
} catch (URISyntaxException exception) {
reportValueError(HtmlWarningCode.INVALID_URI, scriptAttribute, scriptSourcePath);
}
}
node.setScriptElement(script);
scripts.add(script);
}
} finally {
parentNodes.remove(node);
}
return null;
}
@Override
public Void visitHtmlUnit(HtmlUnit node) {
parentNodes = new ArrayList<XmlTagNode>();
scripts = new ArrayList<HtmlScriptElement>();
try {
node.visitChildren(this);
htmlElement.setScripts(scripts.toArray(new HtmlScriptElement[scripts.size()]));
} finally {
scripts = null;
parentNodes = null;
}
return null;
}
@Override
public Void visitXmlAttributeNode(XmlAttributeNode node) {
return null;
}
@Override
public Void visitXmlTagNode(XmlTagNode node) {
if (parentNodes.contains(node)) {
return reportCircularity(node);
}
parentNodes.add(node);
try {
node.visitChildren(this);
} finally {
parentNodes.remove(node);
}
return null;
}
/**
* Return the first source attribute for the given tag node, or {@code null} if it does not exist.
*
* @param node the node containing attributes
* @return the source attribute contained in the given tag
*/
private XmlAttributeNode getScriptSourcePath(XmlTagNode node) {
for (XmlAttributeNode attribute : node.getAttributes()) {
if (attribute.getName().equals(SRC)) {
return attribute;
}
}
return null;
}
private Void reportCircularity(XmlTagNode node) {
//
// This should not be possible, but we have an error report that suggests that it happened at
// least once. This code will guard against infinite recursion and might help us identify the
// cause of the issue.
//
StringBuilder builder = new StringBuilder();
builder.append("Found circularity in XML nodes: ");
boolean first = true;
for (XmlTagNode pathNode : parentNodes) {
if (first) {
first = false;
} else {
builder.append(", ");
}
String tagName = pathNode.getTag();
if (pathNode == node) {
builder.append("*");
builder.append(tagName);
builder.append("*");
} else {
builder.append(tagName);
}
}
AnalysisEngine.getInstance().getLogger().logError(builder.toString());
return null;
}
/**
* Report an error with the given error code at the given location. Use the given arguments to
* compose the error message.
*
* @param errorCode the error code of the error to be reported
* @param offset the offset of the first character to be highlighted
* @param length the number of characters to be highlighted
* @param arguments the arguments used to compose the error message
*/
private void reportErrorForOffset(ErrorCode errorCode, int offset, int length,
Object... arguments) {
errorListener.onError(new AnalysisError(
htmlElement.getSource(),
offset,
length,
errorCode,
arguments));
}
/**
* Report an error with the given error code at the location of the value of the given attribute.
* Use the given arguments to compose the error message.
*
* @param errorCode the error code of the error to be reported
* @param offset the offset of the first character to be highlighted
* @param length the number of characters to be highlighted
* @param arguments the arguments used to compose the error message
*/
private void reportValueError(ErrorCode errorCode, XmlAttributeNode attribute,
Object... arguments) {
int offset = attribute.getValueToken().getOffset() + 1;
int length = attribute.getValueToken().getLength() - 2;
reportErrorForOffset(errorCode, offset, length, arguments);
}
}