dev.amp.validator.Context Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of validator-java Show documentation
Show all versions of validator-java Show documentation
A Java validator for the AMP Html format.
/*
*
* ====================================================================
* 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.
* ====================================================================
*/
/*
* Changes to the original project are Copyright 2019, Yahoo Inc..
*/
package dev.amp.validator;
import dev.amp.validator.css.ParsedDocCssSpec;
import dev.amp.validator.exception.TagValidationException;
import dev.amp.validator.utils.ExtensionsUtils;
import dev.amp.validator.utils.TagSpecUtils;
import dev.amp.validator.utils.ValidationErrorUtils;
import org.xml.sax.Locator;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static dev.amp.validator.utils.AttributeSpecUtils.isUsedForTypeIdentifiers;
/**
* The Context keeps track of the line / column that the validator is
* in, as well as the mandatory tag specs that have already been validated.
* So, this constitutes the mutable state for the validator except for
* the validation result itself.
*
* @author nhant01
* @author GeorgeLuo
*/
public class Context {
/**
* Constructor.
*
* @param parsedValidatorRules a ParsedValidationRules object.
* @param docByteSize size of document
*/
public Context(@Nonnull final ParsedValidatorRules parsedValidatorRules, final int docByteSize) {
this.rules = parsedValidatorRules;
this.mandatoryAlternativesSatisfied = new ArrayList<>();
this.docLocator = null;
this.tagStack = new TagStack();
this.tagspecsValidated = new HashMap<>();
//TODO - it's a hack, remove this when DOCTYPE is fixed
tagspecsValidated.put(0, true);
this.styleTagByteSize = 0;
this.inlineStyleByteSize = 0;
this.typeIdentifiers = new ArrayList<>();
this.valueSetsProvided = new HashSet<>();
this.valueSetsRequired = new HashMap<>();
this.conditionsSatisfied = new HashMap<>();
this.firstUrlSeenTag = null;
this.extensions = new ExtensionsContext();
this.scriptReleaseVersion = ExtensionsUtils.ScriptReleaseVersion.UNKNOWN;
this.docByteSize = docByteSize;
}
/**
* Given the tagResult from validating a single tag, update the overall
* result as well as the Context state to affect later validation.
*
* @param encounteredTag the encountered tag.
* @param referencePointResult the reference point result.
* @param tagResult the tag result.
* @throws TagValidationException the TagValidationException.
*/
public void updateFromTagResults(@Nonnull final ParsedHtmlTag encounteredTag,
@Nonnull final ValidateTagResult referencePointResult,
@Nonnull final ValidateTagResult tagResult) throws TagValidationException {
this.tagStack.updateFromTagResults(
encounteredTag, referencePointResult, tagResult, this.getRules(),
this.getLineCol());
this.recordAttrRequiresExtension(encounteredTag, referencePointResult);
this.recordAttrRequiresExtension(encounteredTag, tagResult);
this.updateFromTagResult(referencePointResult);
this.updateFromTagResult(tagResult);
this.recordScriptReleaseVersionFromTagResult(encounteredTag);
this.addInlineStyleByteSize(tagResult.getInlineStyleCssBytes());
}
/**
* Given a tag result, update the Context state to affect
* later validation. Does not handle updating the tag stack.
*
* @param result a result.
*/
private void updateFromTagResult(@Nonnull final ValidateTagResult result) {
if (result.getBestMatchTagSpec() == null) {
return;
}
final ParsedTagSpec parsedTagSpec = result.getBestMatchTagSpec();
final boolean isPassing =
(result.getValidationResult().getStatus() == ValidatorProtos.ValidationResult.Status.PASS);
this.extensions.updateFromTagResult(result);
// If this requires an extension and we are still in the document head,
// record that we may still need to emit a missing extension error at
// the end of the document head. We do this even for a tag failing
// validation since extensions are based on the tag name, and we're still
// pretty confident the user forgot to include the extension.
if (this.tagStack.hasAncestor("HEAD")) {
this.extensions.recordFutureErrorsIfMissing(
parsedTagSpec, this.getLineCol());
}
// We also want to satisfy conditions, to reduce errors seen elsewhere in
// the document.
this.satisfyConditionsFromTagSpec(parsedTagSpec);
this.satisfyMandatoryAlternativesFromTagSpec(parsedTagSpec);
this.recordValidatedFromTagSpec(isPassing, parsedTagSpec);
final ValidatorProtos.ValidationResult.Builder validationResult = result.getValidationResult();
for (final ValidatorProtos.ValueSetProvision provision : validationResult.getValueSetProvisionsList()) {
this.valueSetsProvided.add(this.keyFromValueSetProvision(provision));
}
for (final ValidatorProtos.ValueSetRequirement requirement : validationResult.getValueSetRequirementsList()) {
if (!requirement.hasProvision()) {
continue;
}
final String key = this.keyFromValueSetProvision(requirement.getProvision());
List errors = this.valueSetsRequired.get(key);
if (errors == null) {
errors = new ArrayList<>();
this.valueSetsRequired.put(key, errors);
}
errors.add(requirement.getErrorIfUnsatisfied());
}
if (isPassing) {
// If the tag spec didn't match, we don't know that the tag actually
// contained a URL, so no need to complain about it.
this.markUrlSeenFromMatchingTagSpec(parsedTagSpec);
}
}
/**
* Record if this document contains a tag requesting the LTS runtime engine.
*
* @param parsedTag
*/
private void recordScriptReleaseVersionFromTagResult(@Nonnull final ParsedHtmlTag parsedTag) {
if (this.getScriptReleaseVersion() == ExtensionsUtils.ScriptReleaseVersion.UNKNOWN
&& (parsedTag.isExtensionScript() || parsedTag.isAmpRuntimeScript())) {
this.scriptReleaseVersion = parsedTag.getScriptReleaseVersion();
}
}
/**
* Records that a Tag was seen which contains an URL. Used to note issues
* with base href occurring in the document after an URL.
*
* @param parsedTagSpec parsed tag spec.
*/
public void markUrlSeenFromMatchingTagSpec(@Nonnull final ParsedTagSpec parsedTagSpec) {
if (!this.hasSeenUrl() && parsedTagSpec.containsUrl()) {
this.firstUrlSeenTag = parsedTagSpec.getSpec();
}
}
/**
* Returns all the value set provisions so far, as a set of derived keys, as
* computed by keyFromValueSetProvision_().
*
* @return the value sets provided
*/
public Set valueSetsProvided() {
return this.valueSetsProvided;
}
/**
* Returns all the value set requirements so far, keyed by derived keys, as
* computed by getValueSetProvisionKey().
*
* @return the map of value sets required.
*/
public Map> valueSetsRequired() {
return this.valueSetsRequired;
}
/**
* Records that this document contains a tag matching a particular tag spec.
*
* @param isPassing is passing status.
* @param parsedTagSpec parsed tag spec.
*/
private void recordValidatedFromTagSpec(final boolean isPassing, @Nonnull final ParsedTagSpec parsedTagSpec) {
final RecordValidated recordValidated = parsedTagSpec.shouldRecordTagspecValidated();
if (recordValidated == RecordValidated.ALWAYS) {
this.tagspecsValidated.put(parsedTagSpec.id(), true);
} else if (isPassing && (recordValidated == RecordValidated.IF_PASSING)) {
this.tagspecsValidated.put(parsedTagSpec.id(), true);
}
}
/**
* Record document-level conditions which have been satisfied.
*
* @param parsedTagSpec parsed tag spec.
*/
private void satisfyConditionsFromTagSpec(@Nonnull final ParsedTagSpec parsedTagSpec) {
for (final String condition : parsedTagSpec.getSpec().getSatisfiesConditionList()) {
this.conditionsSatisfied.put(condition, true);
}
}
/**
* Record that this document contains a tag which is a member of a list
* of mandatory alternatives.
*
* @param parsedTagSpec parsed tag spec.
*/
public void satisfyMandatoryAlternativesFromTagSpec(@Nonnull final ParsedTagSpec parsedTagSpec) {
final ValidatorProtos.TagSpec tagSpec = parsedTagSpec.getSpec();
if (tagSpec.hasMandatoryAlternatives()) {
this.mandatoryAlternativesSatisfied.add(tagSpec.getMandatoryAlternatives());
}
}
/**
* Record when an encountered tag's attribute that requires an extension
* that it also satisfies that the requied extension is used.
*
* @param encounteredTag encountered tag.
* @param tagResult tag result.
*/
private void recordAttrRequiresExtension(@Nonnull final ParsedHtmlTag encounteredTag,
@Nonnull final ValidateTagResult tagResult) {
if (tagResult.getBestMatchTagSpec() == null) {
return;
}
final ParsedTagSpec parsedTagSpec = tagResult.getBestMatchTagSpec();
if (!parsedTagSpec.attrsCanSatisfyExtension()) {
return;
}
Map attrsByName = parsedTagSpec.getAttrsByName();
final ExtensionsContext extensionsCtx = this.extensions;
for (int i = 0; i < encounteredTag.attrs().getLength(); i++) {
String attrName = encounteredTag.attrs().getLocalName(i);
String attrValue = encounteredTag.attrs().getValue(i);
if (attrName.equals(attrValue)) {
attrValue = "";
}
if (attrsByName.containsKey(attrName)) {
final ValidatorProtos.AttrSpec attrSpec = attrsByName.get(attrName);
if (attrSpec == null) {
continue;
}
final ParsedAttrSpec parsedAttrSpec =
this.rules.getParsedAttrSpecs().getParsedAttrSpec(parsedTagSpec.getSpec().getTagName(), attrName, attrValue, attrSpec);
if (parsedAttrSpec != null && parsedAttrSpec.getSpec().getRequiresExtensionCount() > 0) {
extensionsCtx.recordUsedExtensions(
parsedAttrSpec.getSpec().getRequiresExtensionList());
}
}
}
}
/**
* @param error a ValidationError object.
* @param validationResult a ValidationResult object.
*/
public void addBuiltError(@Nonnull final ValidatorProtos.ValidationError error,
@Nonnull final ValidatorProtos.ValidationResult.Builder validationResult) {
// If any of the errors amount to more than a WARNING, validation fails.
if (error.getSeverity() != ValidatorProtos.ValidationError.Severity.WARNING) {
validationResult.setStatus(ValidatorProtos.ValidationResult.Status.FAIL);
}
validationResult.addErrors(error);
}
/**
* Add an error field to validationResult with severity ERROR.
*
* @param validationErrorCode Error code
* @param lineCol a line / column pair.
* @param params a list of params.
* @param specUrl a link (URL) to the amphtml spec
* @param validationResult a ValidationResult object.
*/
public void addError(@Nonnull final ValidatorProtos.ValidationError.Code validationErrorCode,
@Nonnull final Locator lineCol,
final List params, final String specUrl,
@Nonnull final ValidatorProtos.ValidationResult.Builder validationResult) {
addError(validationErrorCode, lineCol.getLineNumber(),
lineCol.getColumnNumber(), params, specUrl, validationResult);
}
/**
* Add an error field to validationResult with severity ERROR.
*
* @param validationErrorCode Error code
* @param line a line number.
* @param column a column number.
* @param params a list of params.
* @param specUrl a link (URL) to the amphtml spec
* @param validationResult a ValidationResult object.
*/
public void addError(@Nonnull final ValidatorProtos.ValidationError.Code validationErrorCode,
final int line,
final int column,
final List params, final String specUrl,
@Nonnull final ValidatorProtos.ValidationResult.Builder validationResult) {
this.addBuiltError(
ValidationErrorUtils.populateError(
ValidatorProtos.ValidationError.Severity.ERROR,
validationErrorCode,
line, column, params, specUrl),
validationResult);
validationResult.setStatus(ValidatorProtos.ValidationResult.Status.FAIL);
}
/**
* Add an error field to validationResult with severity WARNING.
*
* @param validationErrorCode Error code
* @param lineCol a line / column pair.
* @param params a list of params.
* @param specUrl a link (URL) to the amphtml spec
* @param validationResult a ValidationResult object.
*/
public void addWarning(@Nonnull final ValidatorProtos.ValidationError.Code validationErrorCode,
@Nonnull final Locator lineCol,
final List params, final String specUrl,
@Nonnull final ValidatorProtos.ValidationResult.Builder validationResult) {
this.addBuiltError(
ValidationErrorUtils.populateError(
ValidatorProtos.ValidationError.Severity.WARNING, validationErrorCode,
lineCol, params, specUrl),
validationResult);
}
/**
* Returns a line/col pair.
*
* @return returns a line/col pair.
*/
public Locator getLineCol() {
return docLocator;
}
/**
* Setting the LineCol.
*
* @param lineCol a pair line/col.
*/
public void setLineCol(@Nonnull final Locator lineCol) {
this.docLocator = lineCol;
}
/**
* Returns the ParsedValidatorRules.
*
* @return returns the ParsedValidatorRules object.
*/
public ParsedValidatorRules getRules() {
return rules;
}
/**
* Returns the tag stack.
*
* @return returns the tag stack.
*/
public TagStack getTagStack() {
return tagStack;
}
/**
* Record the type identifier in this document.
*
* @param typeIdentifier type identifier.
*/
public void recordTypeIdentifier(@Nonnull final String typeIdentifier) {
this.typeIdentifiers.add(typeIdentifier);
}
/**
* Returns the type identifiers in this document.
*
* @return returns the type identifiers.
*/
public List getTypeIdentifiers() {
return this.typeIdentifiers;
}
/**
* Returns true iff `spec` should be used for the type identifiers recorded
* in this context, as seen in the document so far. If called before type
* identifiers have been recorded, will always return false.
*
* @param spec to evaluate
* @return true iff `spec` should be used for the type identifiers
*/
public boolean isDocSpecValidForTypeIdentifiers(@Nonnull final ParsedDocSpec spec) {
return isUsedForTypeIdentifiers(
this.getTypeIdentifiers(), spec.enabledBy(), spec.disabledBy());
}
/**
* Returns the first (there should be at most one) DocSpec which matches
* both the html format and type identifiers recorded so far in this
* context. If called before identifiers have been recorded, it may return
* an incorrect selection.
*
* @return first (there should be at most one) DocSpec which matches
* both the html format and type identifiers
*/
public ParsedDocSpec matchingDocSpec() {
// The specs are usually already filtered by HTML format, so this loop
// should be very short, often 1:
for (final ParsedDocSpec spec : this.rules.getDoc()) {
if (this.rules.isDocSpecCorrectHtmlFormat(spec.spec())
&& this.isDocSpecValidForTypeIdentifiers(spec)) {
return spec;
}
}
return null;
}
/**
* @return returns the extensions of the current validation job.
*/
public ExtensionsContext getExtensions() {
return extensions;
}
/**
* Returns the tag spec ids that have been validated. The return object
* should be treated as a set (the object keys), and the value should be
* ignored.
*
* @return returns validated tag specs.
*/
public Map getTagspecsValidated() {
return this.tagspecsValidated;
}
/**
* Returns the boolean value of true if exists.
*
* @param id tag spec id.
* @return returns the boolean value of true if exists.
*/
public boolean hasTagspecsValidated(final int id) {
Boolean b = tagspecsValidated.get(id);
if (b == null) {
return false;
}
return b.booleanValue();
}
/**
* Records how much of the document is used towards <style amp-custom>.
*
* @param byteSize byte size.
*/
public void addStyleTagByteSize(final int byteSize) {
this.styleTagByteSize += byteSize;
}
/**
* Records how much of the document is used towards inline style.
*
* @param byteSize integer to add to running inline style byte size.
*/
public void addInlineStyleByteSize(final int byteSize) {
this.inlineStyleByteSize += byteSize;
}
/**
* Returns the size of inline styles.
*
* @return returns running inline style byte size.
*/
public int getInlineStyleByteSize() {
return this.inlineStyleByteSize;
}
/**
* Returns the size of style amp-custom.
*
* @return returns the size of style of amp-custom
*/
public int getStyleTagByteSize() {
return this.styleTagByteSize;
}
/**
* Returns true iff "transformed" is a type identifier in this document.
*
* @return returns true iff "transformed" is a type identifier in this document.
*/
public boolean isTransformed() {
return this.typeIdentifiers.contains("transformed");
}
/**
* Returns true iff the current context has observed a tag which contains
* an URL. This is set by calling markUrlSeen_ above.
*
* @return returns true if first url seen tag is not null.
*/
public boolean hasSeenUrl() {
return this.firstUrlSeenTag != null;
}
/**
* @param condition the condition.
* @return returns true if condition exists.
*/
public boolean satisfiesCondition(@Nonnull final String condition) {
return this.conditionsSatisfied.containsKey(condition);
}
/**
* The TagSpecName of the first seen URL. Do not call unless HasSeenUrl
* returns true.
*
* @return returns TagSpecName of the first seen URL.
*/
public String firstSeenUrlTagName() {
return TagSpecUtils.getTagSpecName(this.firstUrlSeenTag);
}
/**
* The mandatory alternatives that we've satisfied. This may contain
* duplicates (we'd have to filter them in record... above if we cared).
*
* @return returns the mandatory alternatives that we've satisfied.
*/
public List getMandatoryAlternativesSatisfied() {
return this.mandatoryAlternativesSatisfied;
}
/**
* getter for scriptReleaseVersion
*
* @return the associated script release version.
*/
public ExtensionsUtils.ScriptReleaseVersion getScriptReleaseVersion() {
return this.scriptReleaseVersion;
}
/**
* @param provision a ValueSetProvision.
* @return A key for valueSetsProvided and valueSetsRequired.
*/
private String keyFromValueSetProvision(@Nonnull final ValidatorProtos.ValueSetProvision provision) {
return (provision.hasSet() ? provision.getSet() : "")
+ ">"
+ (provision.hasValue() ? provision.getValue() : "");
}
/**
* Returns the first (there should be at most one) DocCssSpec which matches
* both the html format and type identifiers recorded so far in this
* context. If called before identifiers have been recorded, it may return
* an incorrect selection.
*
* @return ParsedDocCssSpec
*/
public ParsedDocCssSpec matchingDocCssSpec() {
// The specs are usually already filtered by HTML format, so this loop
// should be very short, often 1:
for (ParsedDocCssSpec spec : this.rules.getCss()) {
if (this.rules.isDocCssSpecCorrectHtmlFormat(spec.getSpec()) && this.isDocCssSpecValidForTypeIdentifiers(spec)) {
return spec;
}
}
return null;
}
/**
* Returns true iff `spec` should be used for the type identifiers recorded
* in this context, as seen in the document so far. If called before type
* identifiers have been recorded, will always return false.
*
* @param spec
* @return true iff `spec` should be used for the type identifiers recorded in context
*/
private boolean isDocCssSpecValidForTypeIdentifiers(final ParsedDocCssSpec spec) {
return isUsedForTypeIdentifiers(
this.getTypeIdentifiers(), spec.enabledBy(), spec.disabledBy());
}
/**
* Returns the document size from the document locator.
* @return size of document
*/
public int getDocByteSize() {
return this.docByteSize;
}
/**
* An instance of ParsedValidatorRules.
*/
private ParsedValidatorRules rules;
/**
* The mandatory alternatives that we've validated (a small list of ids).
*/
private List mandatoryAlternativesSatisfied;
/**
* DocLocator object from the parser which gives us line/col numbers.
*/
private Locator docLocator = null;
/**
* An instance of TagStack.
*/
private TagStack tagStack = null;
/**
* Set of tagSpec ids that have been validated.
*/
private Map tagspecsValidated;
/**
* Size of <style amp-custom>.
*/
private int styleTagByteSize = 0;
/**
* Size of all inline styles (style attribute) combined.
*/
private int inlineStyleByteSize = 0;
/**
* Set of type identifiers in this document.
*/
private List typeIdentifiers;
/**
* All the value set provisions so far.
*/
private Set valueSetsProvided;
/**
* All the value set requirements so far.
*/
private Map> valueSetsRequired;
/**
* Set of conditions that we've satisfied.
*/
private Map conditionsSatisfied;
/**
* First tag spec seen (matched) which contains an URL.
*/
private ValidatorProtos.TagSpec firstUrlSeenTag = null;
/**
* Extension-specific context.
*/
private ExtensionsContext extensions;
/**
* flag for LTS runtime engine present
*/
private ExtensionsUtils.ScriptReleaseVersion scriptReleaseVersion;
/**
* input html length
*/
private int docByteSize;
}