blob: ff1457513df8707258a76b568363ffcd9a7760ae [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.netbeans.modules.ide.ergonomics.fod;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.regex.Pattern;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathExpression;
import org.w3c.dom.Document;
import org.openide.modules.ModuleInfo;
import org.openide.modules.SpecificationVersion;
import org.openide.util.Lookup;
/** Description of <em>Feature On Demand</em> capabilities and a
* factory to create new instances.
*
* @author Jaroslav Tulach <jtulach@netbeans.org>, Jirka Rechtacek <jrechtacek@netbeans.org>
*/
public final class FeatureInfo {
private final URL delegateLayer;
private final Set<String> cnbs;
private final Map<String,String> nbproject = new HashMap<String,String>();
private final Map<Object[],String> files = new HashMap<Object[],String>();
private Properties properties;
final String clusterName;
private Boolean cacheEnabled;
private Boolean cachePresent;
private FeatureInfo(String clusterName, Set<String> cnbs, URL delegateLayer, Properties p) {
this.cnbs = cnbs;
this.delegateLayer = delegateLayer;
this.properties = p;
this.clusterName = clusterName;
}
public static FeatureInfo create(String clusterName, URL delegateLayer, URL bundle) throws IOException {
Properties p = new Properties();
p.load(bundle.openStream());
String cnbs = p.getProperty("cnbs");
assert cnbs != null : "Error loading from " + bundle; // NOI18N
TreeSet<String> s = new TreeSet<String>();
s.addAll(Arrays.asList(cnbs.split(",")));
FeatureInfo info = new FeatureInfo(clusterName, s, delegateLayer, p);
final String prefix = "nbproject.";
final String prefFile = "project.file.";
final String prefXPath = "project.xpath.";
for (Object k : p.keySet()) {
String key = (String) k;
if (key.startsWith(prefix)) {
info.nbproject(
key.substring(prefix.length()),
p.getProperty(key)
);
}
if (key.startsWith(prefFile)) {
try {
info.projectFile(key.substring(prefFile.length()), null, p.getProperty(key));
} catch (XPathExpressionException ex) {
IOException e = new IOException(ex.getMessage());
e.initCause(ex);
throw e;
}
}
if (key.startsWith(prefXPath)) {
try {
String xpaths = p.getProperty(key);
for (String xp : safeXPathSplit(xpaths)) {
info.projectFile(key.substring(prefXPath.length()), xp, "");
}
} catch (XPathExpressionException ex) {
IOException e = new IOException(ex.getMessage());
e.initCause(ex);
throw e;
}
}
}
return info;
}
public Object getProjectImporter() {
return properties.getProperty("projectImporter");
}
String getPreferredCodeNameBase() {
return properties.getProperty("mainModule");
}
String getFeatureCodeNameBase() {
String f = properties.getProperty("featureModule");
if (f != null) {
return f.length() == 0 ? null : f;
}
return getPreferredCodeNameBase();
}
public final boolean isEnabled() {
Boolean e = cacheEnabled;
if (e != null) {
return e;
}
for (ModuleInfo mi : Lookup.getDefault().lookupAll(ModuleInfo.class)) {
if (cnbs.contains(mi.getCodeNameBase())) {
if (!FeatureManager.showInAU(mi)) {
continue;
}
return cacheEnabled = mi.isEnabled();
}
}
return cacheEnabled = false;
}
public final URL getLayerURL() {
return delegateLayer;
}
/** @return 0 = no
* 1 = yes
* 2 = I am interested to be turned on when this project is opened
*/
int isProject(FeatureProjectFactory.Data data) {
FeatureProjectFactory.LOG.log(Level.FINE, "Checking project {0}", data);
int toRet;
if (isNbProject(data)) {
toRet = 1;
} else {
if (files.isEmpty()) {
toRet = 0;
} else {
toRet = 0;
for (Object[] required : files.keySet()) {
String s = (String)required[0];
FeatureProjectFactory.LOG.log(Level.FINER, " checking file {0}", s);
if (data.hasFile(s)) {
FeatureProjectFactory.LOG.log(Level.FINER, " found", s);
Object r1 = required[1];
if (data.isDeepCheck() && r1 != null) {
XPathExpression e;
if (!(r1 instanceof XPathExpression)) {
try {
final String path = (String) r1;
if (path.isEmpty()) {
toRet = 2;
continue;
}
required[1] = e = XPathFactory.newInstance().newXPath().compile(path);
} catch (XPathExpressionException ex) {
FoDLayersProvider.LOG.log(Level.WARNING, "Cannot parse " + r1, ex);
continue;
}
} else {
e = (XPathExpression)r1;
}
Document content = data.dom(s);
try {
String res = e.evaluate(content);
FeatureProjectFactory.LOG.log(
Level.FINER,
"Parsed result {0} of type {1}",
new Object[] {
res, res == null ? null : res.getClass()
}
);
if (res != null && res.length() > 0) {
toRet = 2;
}
} catch (XPathExpressionException ex) {
FeatureProjectFactory.LOG.log(Level.INFO, "Cannot parse " + data, ex);
}
} else {
toRet = 1;
}
break;
}
}
}
}
FeatureProjectFactory.LOG.log(Level.FINE, " isProject: {0}", toRet);
return toRet;
}
public final Set<String> getCodeNames() {
return Collections.unmodifiableSet(cnbs);
}
public final Set<ExtraModuleInfo> getExtraModules() {
Set<ExtraModuleInfo> s = new LinkedHashSet<>();
for (int i = 0; ; i++) {
String propName = i == 0 ? "extra.modules" : "extra.modules." + i; // NOI18N
String cnbPattern = properties.getProperty(propName);
if (cnbPattern == null) {
break;
}
String recMin = properties.getProperty(propName + ".recommended.min.jdk"); // NOI18N
String recMax = properties.getProperty(propName + ".recommended.max.jdk"); // NOI18N
s.add(new ExtraModuleInfo(cnbPattern, recMin, recMax));
}
return s;
}
static final class ExtraModuleInfo {
private final Pattern cnb;
private final SpecificationVersion recMinJDK;
private final SpecificationVersion recMaxJDK;
ExtraModuleInfo(String cnbPattern, String recMin, String recMax) {
this.cnb = Pattern.compile(cnbPattern);
this.recMinJDK = recMin != null ? new SpecificationVersion(recMin) : null;
this.recMaxJDK = recMax != null ? new SpecificationVersion(recMax) : null;
}
String displayName() {
return cnb.pattern();
}
/**
* Returns the codename, if the pattern is actually a literal.
* Literal pattern should not contain any metacharacters, except '.' and '/'.
* @return codename or {@code null}
*/
String explicitCodebase() {
String pat = cnb.pattern();
for (int a = pat.length() - 1; a >= 0; a--) {
char c = pat.charAt(a);
if (!(Character.isAlphabetic(c) || Character.isDigit(c))) {
if (c != '.' && c != '/') {
return null;
}
}
}
return pat;
}
boolean matches(String cnb) {
return this.cnb.matcher(cnb).matches();
}
final boolean isRequiredFor(SpecificationVersion jdk) {
if (recMinJDK != null && jdk.compareTo(recMinJDK) < 0) {
return true;
}
if (recMaxJDK != null && jdk.compareTo(recMaxJDK) >= 0) {
return true;
}
return false;
}
@Override
public String toString() {
return "ExtraModuleInfo{" + "cnb=" + cnb + ", recMin=" + recMinJDK + ", recMax=" + recMaxJDK + '}';
}
}
public final String getExtraModulesRequiredText() {
return properties.getProperty("LBL_Ergonomics_Extra_Required"); // NOI18N
}
public final String getExtraModulesRecommendedText() {
return properties.getProperty("LBL_Ergonomics_Extra_Recommended"); // NOI18N
}
public boolean isPresent() {
Boolean p = cachePresent;
if (p != null) {
return p;
}
Set<String> codeNames = new HashSet<String>(getCodeNames());
for (ModuleInfo moduleInfo : Lookup.getDefault().lookupAll(ModuleInfo.class)) {
codeNames.remove(moduleInfo.getCodeNameBase());
}
return cachePresent = codeNames.isEmpty();
}
void clearCache() {
cachePresent = null;
cacheEnabled = null;
}
@Override
public String toString() {
return "FeatureInfo[" + clusterName + "]";
}
private boolean isNbProject(FeatureProjectFactory.Data data) {
if (nbproject.isEmpty()) {
return false;
} else {
if (!data.hasFile("nbproject/project.xml")) { // NOI18N
FeatureProjectFactory.LOG.log(Level.FINEST, " nbproject/project.xml not found"); // NOI18N
return false;
}
if (!data.isDeepCheck()) {
FeatureProjectFactory.LOG.log(Level.FINEST, " no deep check, OK"); // NOI18N
return true;
}
String text = data.is("nbproject/project.xml"); // NOI18N
if (text == null) {
return false;
}
for (String t : nbproject.keySet()) {
final String pattern = "<type>" + t + "</type>"; // NOI18N
if (text.indexOf(pattern) >= 0) { // NOI18N
FeatureProjectFactory.LOG.log(Level.FINEST, " '" + pattern + "' found, OK"); // NOI18N
return true;
} else {
FeatureProjectFactory.LOG.log(Level.FINEST, " '" + pattern + "' not found"); // NOI18N
}
}
FeatureProjectFactory.LOG.log(Level.FINEST, " not accepting"); // NOI18N
return false;
}
}
final void nbproject(String prjType, String clazz) {
nbproject.put(prjType, clazz);
}
final void projectFile(String file, String xpath, String clazz) throws XPathExpressionException {
files.put(new Object[] { file, xpath }, clazz);
}
static Map<String,String> nbprojectTypes() {
Map<String,String> map = new HashMap<String, String>();
for (FeatureInfo info : FeatureManager.features()) {
map.putAll(info.nbproject);
}
return map;
}
static Map<String,String> projectFiles() {
Map<String,String> map = new HashMap<String, String>();
for (FeatureInfo info : FeatureManager.features()) {
for (Map.Entry<Object[], String> e : info.files.entrySet()) {
if (e.getValue().length() > 0) {
map.put((String)(e.getKey()[0]), e.getValue());
}
}
}
return map;
}
private static String[] safeXPathSplit(String xpathList) {
List<String> xpaths = new ArrayList<String>();
boolean inSelector = false;
int start = 0, i=0;
for (i=0; i<xpathList.length(); i++) {
char c = xpathList.charAt(i);
switch (c) {
case '[': //NOI18N
inSelector = true;
break;
case ']': //NOI18N
inSelector = false;
break;
case ',': //NOI18N
if (!inSelector) {
String xpath = xpathList.substring(0, i);
start = i + 1;
xpaths.add(xpath);
}
break;
}
}
xpaths.add(xpathList.substring(start, i));
return xpaths.toArray(new String[xpaths.size()]);
}
}