com.aptana.editor.css.validator.CSSValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.aptana.editor.css.validator.CSSValidator.java

Source

/**
 * Aptana Studio
 * Copyright (c) 2005-2012 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
 * Please see the license.html included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.editor.css.validator;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringEscapeUtils;
import org.eclipse.core.runtime.IProgressMonitor;
import org.w3c.css.css.StyleReport;
import org.w3c.css.css.StyleReportFactory;
import org.w3c.css.css.StyleSheet;
import org.w3c.css.css.StyleSheetParser;
import org.w3c.css.properties.PropertiesLoader;
import org.w3c.css.util.ApplContext;
import org.w3c.css.util.Utf8Properties;

import com.aptana.core.build.AbstractBuildParticipant;
import com.aptana.core.build.IProblem;
import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.IOUtil;
import com.aptana.core.util.URLEncoder;
import com.aptana.editor.css.CSSPlugin;
import com.aptana.editor.css.ICSSConstants;
import com.aptana.index.core.build.BuildContext;

/**
 * @author cwilliams
 */
public class CSSValidator extends AbstractBuildParticipant {

    public static final String ID = "com.aptana.editor.css.validator.W3CValidator"; //$NON-NLS-1$

    private static final String APTANA_PROFILE = "AptanaProfile"; //$NON-NLS-1$
    private static final String CONFIG_FILE = "AptanaCSSConfig.properties"; //$NON-NLS-1$
    private static final String PROFILES_CONFIG_FILE = "AptanaCSSProfiles.properties"; //$NON-NLS-1$

    /**
     * error pattern
     */
    private static final Pattern ERROR_PATTERN = Pattern.compile("<(error)>(.*?)</\\1>", //$NON-NLS-1$
            Pattern.MULTILINE | Pattern.DOTALL);
    /**
     * warning pattern
     */
    private static final Pattern WARNING_PATTERN = Pattern.compile("<(warning)>(.*?)</\\1>", //$NON-NLS-1$
            Pattern.MULTILINE | Pattern.DOTALL);

    /**
     * properties pattern
     */
    private static final Pattern PROPERTIES_PATTERN = Pattern.compile("<([-A-Za-z0-9_:]+)>(.*?)</\\1>", //$NON-NLS-1$
            Pattern.MULTILINE | Pattern.DOTALL);

    // CSS3 properties that the validator doesn't recognize yet and need to be ignored
    @SuppressWarnings("nls")
    private static final String[] CSS3_PROPERTIES = { "behavior", "box-shadow", "box-sizing", "column-count",
            "column-width", "column-gap", "column-rule", "border-radius", "background-clip", "background-origin",
            "background-quantity", "background-size", "border-top-right-radius", "border-bottom-right-radius",
            "border-bottom-left-radius", "border-top-left-radius", "font-family", "font-weight", "font-style",
            "outline-offset", "resize", "size", "src", "transform", "transition", "user-select", "word-break" };

    @SuppressWarnings("nls")
    private static final String[] CSS3_AT_RULES = { "@namespace" };

    // other messages that should be filtered automatically
    @SuppressWarnings("nls")
    private static final String[] FILTERED_MESSAGES = { "unrecognized media only", "linear-gradient" };

    @SuppressWarnings("nls")
    public static final String[] DEFAULT_FILTERS = new String[] { ".*Unknown pseudo-element.*",
            "Property\\s*[-_].*doesn't exist.*", ".*-moz-.*", ".*-o-*", ".*opacity.*", ".*overflow-.*",
            ".*accelerator.*", ".*background-position-.*", ".*filter.*", ".*ime-mode.*", ".*layout-.*",
            ".*line-break.*", ".*page.*", ".*ruby-.*", ".*scrollbar-.*", ".*text-align-.*", ".*text-justify.*",
            ".*text-overflow.*", ".*text-shadow.*", ".*text-underline-position.*", ".*word-spacing.*",
            ".*word-wrap.*", ".*writing-mode.*", ".*zoom.*", ".*Parse Error.*", ".*-webkit-.*",
            ".*rgba.*is not a .* value.*", ".*Too many values or values are not recognized.*" };

    static {
        loadAptanaCSSProfile();
    }

    private void processErrorsInReport(String report, String sourcePath, List<IProblem> items,
            List<String> filters) {
        int offset = 0;
        String elementName = "errorlist"; //$NON-NLS-1$
        String startTag = MessageFormat.format("<{0}>", elementName); //$NON-NLS-1$
        String endTag = MessageFormat.format("</{0}>", elementName); //$NON-NLS-1$

        while (offset < report.length()) {
            int errorListStart = report.indexOf(startTag, offset);
            if (errorListStart == -1) {
                break;
            }

            // advances past the start tag
            errorListStart += startTag.length();

            // gets the URI
            int uriStart = report.indexOf("<uri>", errorListStart) + "<uri>".length(); //$NON-NLS-1$ //$NON-NLS-2$
            int uriEnd = report.indexOf("</uri>", uriStart); //$NON-NLS-1$
            String uri = new String(report.substring(uriStart, uriEnd));

            // finds the end of this list
            int errorListEnd = report.indexOf(endTag, errorListStart);

            // checks if the uri matches the source
            if (uri != null && URLEncoder.encode(uri, null, null).equals(sourcePath)) {
                // extracts the error list
                String listString = new String(report.substring(errorListStart, errorListEnd));
                // finds the errors
                String[] errors = getContent(ERROR_PATTERN, listString);
                // add errors
                addErrors(errors, sourcePath, items, filters);
            }

            // advances past the current error list
            offset = errorListEnd + endTag.length();
        }
    }

    private void processWarningsInReport(String report, String sourcePath, List<IProblem> items,
            List<String> filters) {
        int offset = 0;
        String elementName = "warninglist"; //$NON-NLS-1$
        String startTag = MessageFormat.format("<{0}>", elementName); //$NON-NLS-1$
        String endTag = MessageFormat.format("</{0}>", elementName); //$NON-NLS-1$

        while (offset < report.length()) {
            int warningListStart = report.indexOf(startTag, offset);
            if (warningListStart == -1) {
                break;
            }

            // advances past the start tag
            warningListStart += startTag.length();

            // gets the URI
            int uriStart = report.indexOf("<uri>", warningListStart) + "<uri>".length(); //$NON-NLS-1$ //$NON-NLS-2$
            int uriEnd = report.indexOf("</uri>", uriStart); //$NON-NLS-1$
            String uri = new String(report.substring(uriStart, uriEnd));

            // finds the end of this list
            int warningListEnd = report.indexOf(endTag, warningListStart);

            if (uri != null && URLEncoder.encode(uri, null, null).equals(sourcePath)) {
                // extracts the warning list
                String listString = new String(report.substring(warningListStart, warningListEnd));
                // finds the warnings
                String[] warnings = getContent(WARNING_PATTERN, listString);
                // adds errors
                addWarnings(warnings, sourcePath, items, filters);
            }

            // advance past the current warning list
            offset = warningListEnd + endTag.length();
        }
    }

    /**
     * Loads our CSS profile.
     * 
     * @throws IOException
     *             if profile loading fails
     */
    private static void loadAptanaCSSProfile() {
        InputStream configStream = CSSValidator.class.getResourceAsStream(CONFIG_FILE);
        InputStream profilesStream = CSSValidator.class.getResourceAsStream(PROFILES_CONFIG_FILE);

        try {
            // loads our config
            PropertiesLoader.config.load(configStream);

            // loads our profile
            Utf8Properties profiles = new Utf8Properties();
            profiles.load(profilesStream);
            // a hack, but no other way since PropertiesLoader provides no public access to stored profiles
            Field field = PropertiesLoader.class.getDeclaredField("profiles"); //$NON-NLS-1$
            field.setAccessible(true);
            field.set(null, profiles);
        } catch (Exception e) {
            IdeLog.logError(CSSPlugin.getDefault(), Messages.CSSValidator_ERR_FailToLoadProfile, e);
        } finally {
            try {
                configStream.close();
            } catch (IOException e) {
            }
            try {
                profilesStream.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * Adds the CSS errors.
     * 
     * @param errors
     *            the array of errors
     * @param sourcePath
     *            the source path
     * @param manager
     *            the validation manager
     * @param items
     *            the list that stores the added validation items
     */
    private void addErrors(String[] errors, String sourcePath, List<IProblem> items, List<String> filters) {
        Map<String, String> map;
        for (String error : errors) {
            map = getProperties(error);

            int lineNumber = Integer.parseInt(map.get("line")); //$NON-NLS-1$
            String message = map.get("message"); //$NON-NLS-1$
            String context = map.get("context"); //$NON-NLS-1$
            String property = map.get("property"); //$NON-NLS-1$
            String skippedstring = map.get("skippedstring"); //$NON-NLS-1$
            String errorsubtype = map.get("errorsubtype"); //$NON-NLS-1$

            // Don't attempt to add errors if there are already errors on this line
            if (hasErrorOrWarningOnLine(items, lineNumber)) {
                continue;
            }

            if (message == null) {
                if (property == null) {
                    property = context;
                }
                if (skippedstring.equals("[empty string]")) //$NON-NLS-1$
                {
                    // alters the text a bit
                    skippedstring = "no properties defined"; //$NON-NLS-1$
                }
                message = MessageFormat.format("{0} : {1} for {2}", errorsubtype, skippedstring, property); //$NON-NLS-1$
            }
            message = StringEscapeUtils.unescapeHtml(message);
            message = message.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$

            if (!isIgnored(message, filters) && !containsCSS3Property(message) && !containsCSS3AtRule(message)
                    && !isFiltered(message)) {
                // there is no info on the line offset or the length of the errored text
                items.add(createError(message, lineNumber, 0, 0, sourcePath));
            }
        }
    }

    /**
     * Adds the CSS warnings.
     * 
     * @param warnings
     *            the array of warnings
     * @param sourcePath
     *            the source path
     * @param items
     *            the list that stores the added validation items
     * @param filters
     */
    private void addWarnings(String[] warnings, String sourcePath, List<IProblem> items, List<String> filters) {
        Map<String, String> map;
        String last = ""; //$NON-NLS-1$
        for (String warning : warnings) {
            map = getProperties(warning);

            int lineNumber = Integer.parseInt(map.get("line")); //$NON-NLS-1$
            String level = map.get("level"); //$NON-NLS-1$
            String message = MessageFormat.format("{0} (level {1})", map.get("message"), level); //$NON-NLS-1$ //$NON-NLS-2$
            String context = map.get("context"); //$NON-NLS-1$

            // Don't attempt to add warnings if there are already errors on this line
            if (hasErrorOrWarningOnLine(items, lineNumber)) {
                continue;
            }

            String hash = MessageFormat.format("{0}:{1}:{2}:{3}", lineNumber, level, message, context); //$NON-NLS-1$
            // guards against duplicate warnings
            if (!last.equals(hash) && !isIgnored(message, filters)) {
                items.add(createWarning(message, lineNumber, 0, 0, sourcePath));
            }

            last = hash;
        }
    }

    /**
     * Gets the validation report from the validator.
     * 
     * @param source
     *            the source text
     * @param path
     *            the source path
     * @return the report
     */
    private static String getReport(String source, URI path) {
        StyleSheetParser parser = new StyleSheetParser();
        ApplContext ac = new ApplContext("en"); //$NON-NLS-1$
        ac.setProfile(APTANA_PROFILE);
        try {
            parser.parseStyleElement(ac, new ByteArrayInputStream(source.getBytes(IOUtil.UTF_8)), null, null,
                    path.toURL(), 0);
        } catch (MalformedURLException e) {
            IdeLog.logError(CSSPlugin.getDefault(),
                    MessageFormat.format(Messages.CSSValidator_ERR_InvalidPath, path), e);
        } catch (UnsupportedEncodingException e) {
            IdeLog.logError(CSSPlugin.getDefault(), e);
        }

        StyleSheet stylesheet = parser.getStyleSheet();
        stylesheet.findConflicts(ac);
        StyleReport report = StyleReportFactory.getStyleReport(ac, "Title", stylesheet, "soap12", 2); //$NON-NLS-1$ //$NON-NLS-2$
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        report.print(new PrintWriter(out));
        return out.toString().replaceAll("m:", ""); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Gets the list of contents in a source text that matches the specific pattern.
     * 
     * @param pattern
     *            the pattern to match
     * @param source
     *            the source text
     * @return the matching contents in an array
     */
    private static String[] getContent(Pattern pattern, String source) {
        Matcher matcher = pattern.matcher(source);
        List<String> result = new ArrayList<String>();
        while (matcher.find()) {
            result.add(matcher.group(2));
        }
        return result.toArray(new String[result.size()]);
    }

    /**
     * Gets the properties map from a source text.
     * 
     * @param source
     *            the source text
     * @return the properties map
     */
    private static Map<String, String> getProperties(String source) {
        Matcher matcher = PROPERTIES_PATTERN.matcher(source);
        Map<String, String> result = new HashMap<String, String>();
        String key, value;
        while (matcher.find()) {
            key = matcher.group(1);
            value = matcher.group(2);
            if (value != null) {
                value = value.trim();
            }
            result.put(key, value);
        }
        return result;
    }

    private static boolean containsCSS3Property(String message) {
        for (String property : CSS3_PROPERTIES) {
            if (message.indexOf("Property " + property) > -1) //$NON-NLS-1$
            {
                return true;
            }
        }
        return false;
    }

    private static boolean containsCSS3AtRule(String message) {
        for (String rule : CSS3_AT_RULES) {
            if (message.indexOf(MessageFormat.format("the at-rule {0} is not implemented", rule)) > -1) //$NON-NLS-1$
            {
                return true;
            }
        }
        return false;
    }

    private static boolean isFiltered(String message) {
        for (String filtered : FILTERED_MESSAGES) {
            if (message.indexOf(filtered) > -1) {
                return true;
            }
        }
        return false;
    }

    public void buildFile(BuildContext context, IProgressMonitor monitor) {
        if (context == null) {
            return;
        }

        List<IProblem> problems = new ArrayList<IProblem>();

        String source = context.getContents();
        URI uri = context.getURI();
        String path = uri.toString();

        String report = getReport(source, uri);
        List<String> filters = getFilters();
        processErrorsInReport(report, path, problems, filters);
        processWarningsInReport(report, path, problems, filters);

        context.putProblems(ICSSConstants.W3C_PROBLEM, problems);
    }

    public void deleteFile(BuildContext context, IProgressMonitor monitor) {
        if (context == null) {
            return;
        }

        context.removeProblems(ICSSConstants.W3C_PROBLEM);
    }
}