/*
* Copyright 2013-2016 consulo.io
*
* 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 consulo.compiler.impl.resourceCompiler;
import com.intellij.CommonBundle;
import com.intellij.compiler.MalformedPatternException;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.compiler.CompilerBundle;
import com.intellij.openapi.components.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.InputValidator;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import org.apache.oro.text.regex.*;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
/**
* @author VISTALL
* @since 20:16/24.05.13
*/
@State(
name = "ResourceCompilerConfiguration",
storages = {
@Storage(file = StoragePathMacros.PROJECT_CONFIG_DIR + "/compiler.xml")})
public class ResourceCompilerConfiguration implements PersistentStateComponent<Element> {
@NotNull
public static ResourceCompilerConfiguration getInstance(@NotNull Project project) {
return ServiceManager.getService(project, ResourceCompilerConfiguration.class);
}
public static final Logger LOGGER = Logger.getInstance(ResourceCompilerConfiguration.class);
public static final String RESOURCE_EXTENSIONS = "resourceExtensions";
public static final String WILDCARD_RESOURCE_PATTERNS = "wildcardResourcePatterns";
public static final String ENTRY = "entry";
public static final String NAME = "name";
private static class CompiledPattern {
@NotNull final Pattern fileName;
@Nullable final Pattern dir;
@Nullable final Pattern srcRoot;
private CompiledPattern(Pattern fileName, Pattern dir, Pattern srcRoot) {
this.fileName = fileName;
this.dir = dir;
this.srcRoot = srcRoot;
}
}
private Project myProject;
// extensions of the files considered as resource files
private final List<Pattern> myRegexpResourcePatterns = new ArrayList<Pattern>();
// extensions of the files considered as resource files. If present, overrides patterns in old regexp format stored in myRegexpResourcePatterns
private final List<String> myWildcardPatterns = new ArrayList<String>();
private final List<CompiledPattern> myCompiledPatterns = new ArrayList<CompiledPattern>();
private final List<CompiledPattern> myNegatedCompiledPatterns = new ArrayList<CompiledPattern>();
private boolean myWildcardPatternsInitialized = false;
private final Perl5Matcher myPatternMatcher = new Perl5Matcher();
public ResourceCompilerConfiguration(Project project) {
myProject = project;
}
public boolean isResourceFile(VirtualFile virtualFile) {
return isResourceFile(virtualFile.getName(), virtualFile.getParent());
}
public List<String> getResourceFilePatterns() {
return new ArrayList<String>(myWildcardPatterns);
}
public void removeResourceFilePatterns() {
removeWildcardPatterns();
}
private boolean isResourceFile(String name, @Nullable VirtualFile parent) {
final Ref<String> parentRef = Ref.create(null);
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < myCompiledPatterns.size(); i++) {
if (matches(name, parent, parentRef, myCompiledPatterns.get(i))) {
return true;
}
}
if (myNegatedCompiledPatterns.isEmpty()) {
return false;
}
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < myNegatedCompiledPatterns.size(); i++) {
if (matches(name, parent, parentRef, myNegatedCompiledPatterns.get(i))) {
return false;
}
}
return true;
}
private boolean matches(String name, VirtualFile parent, Ref<String> parentRef, CompiledPattern pair) {
if (!matches(name, pair.fileName)) {
return false;
}
if (parent != null && (pair.dir != null || pair.srcRoot != null)) {
VirtualFile srcRoot = ProjectRootManager.getInstance(myProject).getFileIndex().getSourceRootForFile(parent);
if (pair.dir != null) {
String parentPath = parentRef.get();
if (parentPath == null) {
parentRef.set(parentPath = srcRoot == null ? parent.getPath() : VfsUtilCore.getRelativePath(parent, srcRoot, '/'));
}
if (parentPath == null || !matches("/" + parentPath, pair.dir)) {
return false;
}
}
if (pair.srcRoot != null) {
String srcRootName = srcRoot == null ? null : srcRoot.getName();
if (srcRootName == null || !matches(srcRootName, pair.srcRoot)) {
return false;
}
}
}
return true;
}
private boolean matches(String s, Pattern p) {
synchronized (myPatternMatcher) {
try {
return myPatternMatcher.matches(s, p);
}
catch (Exception e) {
ResourceCompilerConfiguration.LOGGER.error("Exception matching file name \"" + s + "\" against the pattern \"" + p + "\"", e);
return false;
}
}
}
public void convertPatterns() {
if (!needPatternConversion()) {
return;
}
try {
boolean ok;
try {
ok = doConvertPatterns();
}
catch (MalformedPatternException e) {
ok = false;
}
if (!ok) {
final String initialPatternString = patternsToString(getRegexpPatterns());
final String message = CompilerBundle
.message("message.resource.patterns.format.changed", ApplicationNamesInfo.getInstance().getProductName(), initialPatternString,
CommonBundle.getOkButtonText(), CommonBundle.getCancelButtonText());
final String wildcardPatterns = Messages
.showInputDialog(myProject, message, CompilerBundle.message("pattern.conversion.dialog.title"), Messages.getWarningIcon(),
initialPatternString, new InputValidator() {
public boolean checkInput(String inputString) {
return true;
}
public boolean canClose(String inputString) {
final StringTokenizer tokenizer = new StringTokenizer(inputString, ";", false);
StringBuilder malformedPatterns = new StringBuilder();
while (tokenizer.hasMoreTokens()) {
String pattern = tokenizer.nextToken();
try {
addWildcardResourcePattern(pattern);
}
catch (MalformedPatternException e) {
malformedPatterns.append("\n\n");
malformedPatterns.append(pattern);
malformedPatterns.append(": ");
malformedPatterns.append(e.getMessage());
}
}
if (malformedPatterns.length() > 0) {
Messages.showErrorDialog(CompilerBundle.message("error.bad.resource.patterns", malformedPatterns.toString()),
CompilerBundle.message("bad.resource.patterns.dialog.title"));
removeWildcardPatterns();
return false;
}
return true;
}
});
}
}
finally {
myWildcardPatternsInitialized = true;
}
}
private void removeWildcardPatterns() {
myWildcardPatterns.clear();
myCompiledPatterns.clear();
myNegatedCompiledPatterns.clear();
}
public void addResourceFilePattern(String namePattern) throws MalformedPatternException {
addWildcardResourcePattern(namePattern);
}
private void addWildcardResourcePattern(@NonNls final String wildcardPattern) throws MalformedPatternException {
final CompiledPattern pattern = convertToRegexp(wildcardPattern);
if (pattern != null) {
myWildcardPatterns.add(wildcardPattern);
if (isPatternNegated(wildcardPattern)) {
myNegatedCompiledPatterns.add(pattern);
}
else {
myCompiledPatterns.add(pattern);
}
}
}
private boolean needPatternConversion() {
return !myWildcardPatternsInitialized && !myRegexpResourcePatterns.isEmpty();
}
private String[] getRegexpPatterns() {
String[] patterns = ArrayUtil.newStringArray(myRegexpResourcePatterns.size());
int index = 0;
for (final Pattern myRegexpResourcePattern : myRegexpResourcePatterns) {
patterns[index++] = myRegexpResourcePattern.getPattern();
}
return patterns;
}
private boolean doConvertPatterns() throws MalformedPatternException {
final String[] regexpPatterns = getRegexpPatterns();
final List<String> converted = new ArrayList<>();
final Pattern multipleExtensionsPatternPattern = compilePattern("\\.\\+\\\\\\.\\((\\w+(?:\\|\\w+)*)\\)");
final Pattern singleExtensionPatternPattern = compilePattern("\\.\\+\\\\\\.(\\w+)");
final Perl5Matcher matcher = new Perl5Matcher();
for (final String regexpPattern : regexpPatterns) {
//final Matcher multipleExtensionsMatcher = multipleExtensionsPatternPattern.matcher(regexpPattern);
if (matcher.matches(regexpPattern, multipleExtensionsPatternPattern)) {
final MatchResult match = matcher.getMatch();
final StringTokenizer tokenizer = new StringTokenizer(match.group(1), "|", false);
while (tokenizer.hasMoreTokens()) {
converted.add("?*." + tokenizer.nextToken());
}
}
else {
//final Matcher singleExtensionMatcher = singleExtensionPatternPattern.matcher(regexpPattern);
if (matcher.matches(regexpPattern, singleExtensionPatternPattern)) {
final MatchResult match = matcher.getMatch();
converted.add("?*." + match.group(1));
}
else {
return false;
}
}
}
for (final String aConverted : converted) {
addWildcardResourcePattern(aConverted);
}
return true;
}
private static Pattern compilePattern(@NonNls String s) throws MalformedPatternException {
try {
final PatternCompiler compiler = new Perl5Compiler();
return SystemInfo.isFileSystemCaseSensitive ? compiler.compile(s) : compiler.compile(s, Perl5Compiler.CASE_INSENSITIVE_MASK);
}
catch (org.apache.oro.text.regex.MalformedPatternException ex) {
throw new MalformedPatternException(ex);
}
}
public static boolean isPatternNegated(String wildcardPattern) {
return wildcardPattern.length() > 1 && wildcardPattern.charAt(0) == '!';
}
public static CompiledPattern convertToRegexp(String wildcardPattern) throws MalformedPatternException{
if (isPatternNegated(wildcardPattern)) {
wildcardPattern = wildcardPattern.substring(1);
}
wildcardPattern = FileUtil.toSystemIndependentName(wildcardPattern);
String srcRoot = null;
int colon = wildcardPattern.indexOf(":");
if (colon > 0) {
srcRoot = wildcardPattern.substring(0, colon);
wildcardPattern = wildcardPattern.substring(colon + 1);
}
String dirPattern = null;
int slash = wildcardPattern.lastIndexOf('/');
if (slash >= 0) {
dirPattern = wildcardPattern.substring(0, slash + 1);
wildcardPattern = wildcardPattern.substring(slash + 1);
if (!dirPattern.startsWith("/")) {
dirPattern = "/" + dirPattern;
}
//now dirPattern starts and ends with '/'
dirPattern = normalizeWildcards(dirPattern);
dirPattern = StringUtil.replace(dirPattern, "/.*.*/", "(/.*)?/");
dirPattern = StringUtil.trimEnd(dirPattern, "/");
dirPattern = optimize(dirPattern);
}
wildcardPattern = normalizeWildcards(wildcardPattern);
wildcardPattern = optimize(wildcardPattern);
final Pattern dirCompiled = dirPattern == null ? null : compilePattern(dirPattern);
final Pattern srcCompiled = srcRoot == null ? null : compilePattern(optimize(normalizeWildcards(srcRoot)));
return new CompiledPattern(compilePattern(wildcardPattern), dirCompiled, srcCompiled);
}
private static String optimize(String wildcardPattern) {
return wildcardPattern.replaceAll("(?:\\.\\*)+", ".*");
}
private static String normalizeWildcards(String wildcardPattern) {
wildcardPattern = StringUtil.replace(wildcardPattern, "\\!", "!");
wildcardPattern = StringUtil.replace(wildcardPattern, ".", "\\.");
wildcardPattern = StringUtil.replace(wildcardPattern, "*?", ".+");
wildcardPattern = StringUtil.replace(wildcardPattern, "?*", ".+");
wildcardPattern = StringUtil.replace(wildcardPattern, "*", ".*");
wildcardPattern = StringUtil.replace(wildcardPattern, "?", ".");
return wildcardPattern;
}
private static String patternsToString(final String[] patterns) {
final StringBuilder extensionsString = new StringBuilder();
for (int idx = 0; idx < patterns.length; idx++) {
if (idx > 0) {
extensionsString.append(";");
}
extensionsString.append(patterns[idx]);
}
return extensionsString.toString();
}
@Nullable
@Override
public Element getState() {
String[] patterns = getRegexpPatterns();
if(patterns.length == 0 && myWildcardPatterns.isEmpty()) {
return null;
}
Element state = new Element("state");
final Element newChild = addChild(state, RESOURCE_EXTENSIONS);
for (final String pattern : patterns) {
addChild(newChild, ENTRY).setAttribute(NAME, pattern);
}
if (myWildcardPatternsInitialized || !myWildcardPatterns.isEmpty()) {
final Element wildcardPatterns = addChild(state, WILDCARD_RESOURCE_PATTERNS);
for (final String wildcardPattern : myWildcardPatterns) {
addChild(wildcardPatterns, ENTRY)
.setAttribute(NAME, wildcardPattern);
}
}
return state;
}
@Override
public void loadState(Element state) {
readExternal(state);
}
private void removeRegexpPatterns() {
myRegexpResourcePatterns.clear();
}
private void addRegexpPattern(String namePattern) throws MalformedPatternException {
Pattern pattern = compilePattern(namePattern);
if (pattern != null) {
myRegexpResourcePatterns.add(pattern);
}
}
public void readExternal(Element parentNode) {
try {
removeRegexpPatterns();
Element node = parentNode.getChild(RESOURCE_EXTENSIONS);
if (node != null) {
for (final Element element : node.getChildren(ENTRY)) {
String pattern = element.getAttributeValue(NAME);
if (!StringUtil.isEmpty(pattern)) {
addRegexpPattern(pattern);
}
}
}
removeWildcardPatterns();
node = parentNode.getChild(WILDCARD_RESOURCE_PATTERNS);
if (node != null) {
myWildcardPatternsInitialized = true;
for (final Element element : node.getChildren(ENTRY)) {
String pattern = element.getAttributeValue(NAME);
if (!StringUtil.isEmpty(pattern)) {
addWildcardResourcePattern(pattern);
}
}
}
}
catch (MalformedPatternException e) {
throw new InvalidDataException(e);
}
}
private static Element addChild(Element parent, final String childName) {
final Element child = new Element(childName);
parent.addContent(child);
return child;
}
}