blob: bdb9a610661ce26fe79e699aa61350343bdb3694 [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.php.project;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.modules.php.api.util.StringUtils;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.EditableProperties;
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
/**
* Represents a helper for manipulation source roots.
* Based on SourceRoot class (common.java.api)
* which was copied to all non java projecs.
* <p>
* For PHP project, it's simplified because there's no need
* to store source roots in project.xml (they don't need
* to be propagated to any build.xml or so). In fact, project.xml
* is not interesting for PHP project at all.
* @author Tomas Zezula, Tomas Mysik
*/
public final class SourceRoots {
/**
* Property name of a event that is fired when project properties change.
*/
public static final String PROP_ROOTS = SourceRoots.class.getName() + ".roots"; //NOI18N
private final UpdateHelper helper;
private final PropertyEvaluator evaluator;
private final String displayName;
private final String propertyNumericPrefix;
private final PropertyChangeSupport support;
private final ProjectMetadataListener listener;
private final boolean tests;
// #196060 - help to diagnose
private final AtomicLong firedChanges = new AtomicLong();
// @GuardedBy("this")
private List<FileObject> sourceRoots;
// @GuardedBy("this")
private List<URL> sourceRootUrls;
// @GuardedBy("this")
private List<String> sourceRootProperties;
// @GuardedBy("this")
private List<String> pureSourceRootNames;
private SourceRoots(Builder builder) {
assert builder.helper != null;
assert builder.evaluator != null;
assert builder.displayName != null;
helper = builder.helper;
evaluator = builder.evaluator;
displayName = builder.displayName;
propertyNumericPrefix = builder.propertyNumericPrefix;
tests = builder.tests;
sourceRootProperties = builder.properties;
support = new PropertyChangeSupport(this);
listener = new ProjectMetadataListener();
}
static SourceRoots create(Builder builder) {
SourceRoots roots = new SourceRoots(builder);
roots.evaluator.addPropertyChangeListener(WeakListeners.propertyChange(roots.listener, roots.evaluator));
return roots;
}
/**
* Returns the display names of source roots.
* The returned array has the same length as an array returned by the {@link #getRootProperties()}.
* It may contain empty {@link String}s but not <code>null</code>.
* @return an array of source roots names.
*/
@NbBundle.Messages({
"# {0} - display name of the source root",
"# {1} - directory of the source root",
"SourceRoots.displayName={0} ({1})",
})
public String[] getRootNames() {
String[] pureRootNames = getPureRootNames();
if (pureRootNames.length == 0) {
return new String[0];
}
String[] names = new String[pureRootNames.length];
for (int i = 0; i < names.length; i++) {
String pureName = pureRootNames[i];
String name;
if (StringUtils.hasText(pureName)) {
name = Bundle.SourceRoots_displayName(displayName, pureName);
} else {
name = displayName;
}
names[i] = name;
}
return names;
}
/**
* Returns the pure display names of source roots.
* The returned array has the same length as an array returned by the {@link #getRootProperties()}.
* It may contain empty {@link String}s but not <code>null</code>.
* @return an array of pure source roots names.
*/
public synchronized String[] getPureRootNames() {
return ProjectManager.mutex().readAccess(new Mutex.Action<String[]>() {
@Override
public String[] run() {
synchronized (SourceRoots.this) {
assert Thread.holdsLock(SourceRoots.this);
if (pureSourceRootNames == null) {
List<String> dirPaths = new ArrayList<>();
for (String property : getRootProperties()) {
String path = evaluator.getProperty(property);
if (path == null) {
dirPaths.add(null);
} else {
dirPaths.add(helper.getAntProjectHelper().resolvePath(path));
}
}
pureSourceRootNames = getPureSourceRootsNames(dirPaths);
}
return pureSourceRootNames.toArray(new String[pureSourceRootNames.size()]);
}
}
});
}
/**
* Returns names of Ant properties in the <i>project.properties</i> file holding the source roots.
* @return an array of String.
*/
public String[] getRootProperties() {
return ProjectManager.mutex().readAccess(new Mutex.Action<String[]>() {
@Override
public String[] run() {
synchronized (SourceRoots.this) {
assert Thread.holdsLock(SourceRoots.this);
if (sourceRootProperties == null) {
assert propertyNumericPrefix != null : displayName;
sourceRootProperties = new ArrayList<>();
EditableProperties projectProperties = helper.getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH);
// #246368
if (projectProperties.containsKey(propertyNumericPrefix)) {
sourceRootProperties.add(propertyNumericPrefix);
}
int i = 1;
while (true) {
String key = propertyNumericPrefix + i;
if (projectProperties.containsKey(key)) {
sourceRootProperties.add(key);
} else if (i > 1) {
break;
}
i++;
}
}
return sourceRootProperties.toArray(new String[sourceRootProperties.size()]);
}
}
});
}
/**
* Returns the source roots in the form of absolute paths.
* @return an array of {@link FileObject}s.
*/
public FileObject[] getRoots() {
return ProjectManager.mutex().readAccess(new Mutex.Action<FileObject[]>() {
@Override
public FileObject[] run() {
synchronized (SourceRoots.this) {
// local caching
assert Thread.holdsLock(SourceRoots.this);
if (sourceRoots == null) {
String[] srcProps = getRootProperties();
List<FileObject> result = new ArrayList<>();
for (String p : srcProps) {
String prop = evaluator.getProperty(p);
if (prop != null) {
FileObject f = helper.getAntProjectHelper().resolveFileObject(prop);
if (f == null) {
continue;
}
if (FileUtil.isArchiveFile(f)) {
f = FileUtil.getArchiveRoot(f);
}
result.add(f);
}
}
sourceRoots = Collections.unmodifiableList(result);
}
return sourceRoots.toArray(new FileObject[sourceRoots.size()]);
}
}
});
}
/**
* Returns the source roots as {@link URL}s.
* @return an array of {@link URL}.
*/
public URL[] getRootURLs() {
return ProjectManager.mutex().readAccess(new Mutex.Action<URL[]>() {
@Override
public URL[] run() {
synchronized (SourceRoots.this) {
assert Thread.holdsLock(SourceRoots.this);
// local caching
if (sourceRootUrls == null) {
List<URL> result = new ArrayList<>();
for (String srcProp : getRootProperties()) {
String prop = evaluator.getProperty(srcProp);
if (prop != null) {
File f = FileUtil.normalizeFile(helper.getAntProjectHelper().resolveFile(prop));
try {
URL url = Utilities.toURI(f).toURL();
if (!f.exists()) {
url = new URL(url.toExternalForm() + "/"); // NOI18N
} else if (f.isFile()) {
// file cannot be a source root (archives are not supported as source roots).
continue;
}
assert url.toExternalForm().endsWith("/") : "#90639 violation for " + url + "; "
+ f + " exists? " + f.exists() + " dir? " + f.isDirectory()
+ " file? " + f.isFile();
result.add(url);
} catch (MalformedURLException e) {
Exceptions.printStackTrace(e);
}
}
}
sourceRootUrls = Collections.unmodifiableList(result);
}
return sourceRootUrls.toArray(new URL[sourceRootUrls.size()]);
}
}
});
}
/**
* Adds {@link PropertyChangeListener}, see class description for more information
* about listening to the source roots changes.
* @param listener a listener to add.
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
/**
* Removes {@link PropertyChangeListener}, see class description for more information
* about listening to the source roots changes.
* @param listener a listener to remove.
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
/**
* Translates root name into display name of source/test root.
* @param rootName the name of root got from {@link SourceRoots#getRootNames()}.
* @param propName the name of a property the root is stored in.
* @return the label to be displayed.
*/
public String getRootDisplayName(String rootName, String propName) {
assert StringUtils.hasText(rootName) : "No name for " + propName; // NOI18N
return rootName;
}
/**
* Returns <code>true</code> if the current {@link SourceRoots} instance represents source roots belonging to
* the test compilation unit.
* @return boolean <code>true</code> if the instance belongs to the test compilation unit, false otherwise.
*/
public boolean isTest() {
return tests;
}
private void resetCache(String propName) {
boolean fire = false;
synchronized (this) {
assert Thread.holdsLock(this);
// in case of change reset local cache
if (propName == null
|| (sourceRootProperties != null && sourceRootProperties.contains(propName))
|| (propertyNumericPrefix != null && propName.startsWith(propertyNumericPrefix))) {
sourceRoots = null;
sourceRootUrls = null;
if (propertyNumericPrefix != null) {
sourceRootProperties = null;
}
pureSourceRootNames = null;
fire = true;
}
}
if (fire) {
firedChanges.incrementAndGet();
support.firePropertyChange(PROP_ROOTS, null, null);
}
}
public void refresh() {
resetCache(null);
}
public long getFiredChanges() {
return firedChanges.get();
}
static List<String> getPureSourceRootsNames(List<String> dirPaths) {
if (dirPaths.isEmpty()) {
return Collections.emptyList();
}
if (dirPaths.size() == 1) {
return Collections.singletonList(""); // NOI18N
}
if (checkIncorrectValues(dirPaths)) {
// incorrect, duplicated values (should not happen)
List<String> names = new ArrayList<>(dirPaths.size());
for (String path : dirPaths) {
if (path == null) {
names.add(""); // NOI18N
} else {
names.add(path);
}
}
return names;
}
String[] names = new String[dirPaths.size()];
int lastIndex = 0;
List<Integer> duplicated = new ArrayList<>(dirPaths.size());
for (;;) {
duplicated.clear();
for (int i = 0; i < dirPaths.size(); i++) {
if (names[i] != null) {
// already set
continue;
}
String path = dirPaths.get(i);
if (path == null) {
names[i] = ""; // NOI18N
} else {
List<String> segments = StringUtils.explode(path, File.separator);
int index = segments.size() - 1 - lastIndex;
if (index < 0) {
index = 0;
}
String name;
if (index >= segments.size()) {
// should not happen... corrupted metadata?
name = "???"; // NOI18N
} else {
name = segments.get(index);
}
int indexOf = Arrays.asList(names).indexOf(name);
if (indexOf != -1
&& indexOf != i) {
duplicated.add(indexOf);
} else {
names[i] = name;
}
}
}
for (Integer index : duplicated) {
names[index] = null;
}
boolean finished = true;
for (String name : names) {
if (name == null) {
finished = false;
break;
}
}
if (finished) {
break;
}
lastIndex++;
}
return Arrays.asList(names);
}
private static boolean checkIncorrectValues(List<String> dirPaths) {
List<String> copy = new ArrayList<>(dirPaths);
copy.removeAll(Collections.singleton(null));
return new HashSet<>(copy).size() != copy.size();
}
//~ Inner classes
public static final class Builder {
final UpdateHelper helper;
final PropertyEvaluator evaluator;
final String displayName;
List<String> properties;
String propertyNumericPrefix;
boolean tests;
Builder(UpdateHelper helper, PropertyEvaluator evaluator, String displayName) {
this.helper = helper;
this.evaluator = evaluator;
this.displayName = displayName;
}
public Builder setProperties(List<String> properties) {
this.properties = properties;
return this;
}
public Builder setProperties(String... properties) {
this.properties = Arrays.asList(properties);
return this;
}
public Builder setPropertyNumericPrefix(String propertyNumericPrefix) {
this.propertyNumericPrefix = propertyNumericPrefix;
return this;
}
public Builder setTests(boolean tests) {
this.tests = tests;
return this;
}
public SourceRoots build() {
assert properties != null || propertyNumericPrefix != null;
return SourceRoots.create(this);
}
//~ Factories
public static Builder create(UpdateHelper helper, PropertyEvaluator evaluator, String displayName) {
assert helper != null;
assert evaluator != null;
return new Builder(helper, evaluator, displayName);
}
}
private final class ProjectMetadataListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
resetCache(evt.getPropertyName());
}
}
}