blob: 7b5de892d4dafbd100e66f56990db78ca814204c [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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 com.intellij.openapi.roots.impl.libraries;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ComponentSerializationUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.RootProvider;
import com.intellij.openapi.roots.impl.RootModelImpl;
import com.intellij.openapi.roots.impl.RootProviderBaseImpl;
import com.intellij.openapi.roots.libraries.*;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.StandardFileSystems;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.openapi.vfs.pointers.VirtualFilePointer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerContainer;
import com.intellij.openapi.vfs.pointers.VirtualFilePointerManager;
import com.intellij.util.ArrayUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.xmlb.SkipDefaultValuesSerializationFilters;
import com.intellij.util.xmlb.XmlSerializer;
import gnu.trove.THashSet;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.intellij.openapi.vfs.VirtualFileVisitor.ONE_LEVEL_DEEP;
import static com.intellij.openapi.vfs.VirtualFileVisitor.SKIP_ROOT;
/**
* @author dsl
*/
public class LibraryImpl extends TraceableDisposable implements LibraryEx.ModifiableModelEx, LibraryEx {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.roots.impl.impl.LibraryImpl");
@NonNls public static final String LIBRARY_NAME_ATTR = "name";
@NonNls public static final String LIBRARY_TYPE_ATTR = "type";
@NonNls public static final String ROOT_PATH_ELEMENT = "root";
@NonNls public static final String ELEMENT = "library";
@NonNls public static final String PROPERTIES_ELEMENT = "properties";
private static final SkipDefaultValuesSerializationFilters SERIALIZATION_FILTERS = new SkipDefaultValuesSerializationFilters();
private static final String EXCLUDED_ROOTS_TAG = "excluded";
private String myName;
private final LibraryTable myLibraryTable;
private final Map<OrderRootType, VirtualFilePointerContainer> myRoots;
@Nullable private VirtualFilePointerContainer myExcludedRoots;
private final JarDirectories myJarDirectories = new JarDirectories();
private final LibraryImpl mySource;
private PersistentLibraryKind<?> myKind;
private LibraryProperties myProperties;
private final MyRootProviderImpl myRootProvider = new MyRootProviderImpl();
private final ModifiableRootModel myRootModel;
private boolean myDisposed;
private final Disposable myPointersDisposable = Disposer.newDisposable();
private final JarDirectoryWatcher myRootsWatcher = JarDirectoryWatcherFactory.getInstance().createWatcher(myJarDirectories, myRootProvider);
LibraryImpl(LibraryTable table, Element element, ModifiableRootModel rootModel) throws InvalidDataException {
this(table, rootModel, null, element.getAttributeValue(LIBRARY_NAME_ATTR),
(PersistentLibraryKind<?>)LibraryKind.findById(element.getAttributeValue(LIBRARY_TYPE_ATTR)));
readProperties(element);
myJarDirectories.readExternal(element);
readRoots(element);
myRootsWatcher.updateWatchedRoots();
}
LibraryImpl(String name, @Nullable final PersistentLibraryKind<?> kind, LibraryTable table, ModifiableRootModel rootModel) {
this(table, rootModel, null, name, kind);
if (kind != null) {
myProperties = kind.createDefaultProperties();
}
}
private LibraryImpl(@NotNull LibraryImpl from, LibraryImpl newSource, ModifiableRootModel rootModel) {
this(from.myLibraryTable, rootModel, newSource, from.myName, from.myKind);
from.checkDisposed();
if (from.myKind != null && from.myProperties != null) {
myProperties = myKind.createDefaultProperties();
//noinspection unchecked
myProperties.loadState(from.myProperties.getState());
}
for (OrderRootType rootType : getAllRootTypes()) {
final VirtualFilePointerContainer thisContainer = myRoots.get(rootType);
final VirtualFilePointerContainer thatContainer = from.myRoots.get(rootType);
thisContainer.addAll(thatContainer);
}
if (from.myExcludedRoots != null) {
myExcludedRoots = from.myExcludedRoots.clone(myPointersDisposable);
}
myJarDirectories.copyFrom(from.myJarDirectories);
}
// primary
private LibraryImpl(LibraryTable table, ModifiableRootModel rootModel, LibraryImpl newSource, String name, @Nullable final PersistentLibraryKind<?> kind) {
super(new Throwable());
myLibraryTable = table;
myRootModel = rootModel;
mySource = newSource;
myKind = kind;
myName = name;
//init roots depends on my myKind
myRoots = initRoots();
Disposer.register(this, myRootsWatcher);
}
private Set<OrderRootType> getAllRootTypes() {
Set<OrderRootType> rootTypes = new HashSet<OrderRootType>();
rootTypes.addAll(Arrays.asList(OrderRootType.getAllTypes()));
if (myKind != null) {
rootTypes.addAll(Arrays.asList(myKind.getAdditionalRootTypes()));
}
return rootTypes;
}
@Override
public void dispose() {
checkDisposed();
myDisposed = true;
kill(null);
}
private void checkDisposed() {
if (isDisposed()) {
throwDisposalError("'" + myName + "' already disposed:");
}
}
@Override
public boolean isDisposed() {
return myDisposed;
}
@Override
public String getName() {
return myName;
}
@Override
@NotNull
public String[] getUrls(@NotNull OrderRootType rootType) {
checkDisposed();
final VirtualFilePointerContainer result = myRoots.get(rootType);
return result.getUrls();
}
@Override
@NotNull
public VirtualFile[] getFiles(@NotNull OrderRootType rootType) {
checkDisposed();
final List<VirtualFile> expanded = new ArrayList<VirtualFile>();
for (VirtualFile file : myRoots.get(rootType).getFiles()) {
if (file.isDirectory()) {
if (myJarDirectories.contains(rootType, file.getUrl())) {
collectJarFiles(file, expanded, myJarDirectories.isRecursive(rootType, file.getUrl()));
continue;
}
}
expanded.add(file);
}
return VfsUtilCore.toVirtualFileArray(expanded);
}
public static void collectJarFiles(final VirtualFile dir, final List<VirtualFile> container, final boolean recursively) {
VfsUtilCore.visitChildrenRecursively(dir, new VirtualFileVisitor(SKIP_ROOT, recursively ? null : ONE_LEVEL_DEEP) {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
final VirtualFile jarRoot = file.isDirectory() ? null : StandardFileSystems.getJarRootForLocalFile(file);
if (jarRoot != null) {
container.add(jarRoot);
return false;
}
return true;
}
});
}
@Override
public void setName(String name) {
LOG.assertTrue(isWritable());
myName = name;
}
/* you have to commit modifiable model or dispose it by yourself! */
@Override
@NotNull
public ModifiableModelEx getModifiableModel() {
checkDisposed();
return new LibraryImpl(this, this, myRootModel);
}
@Override
public Library cloneLibrary(RootModelImpl rootModel) {
LOG.assertTrue(myLibraryTable == null);
final LibraryImpl clone = new LibraryImpl(this, null, rootModel);
clone.myRootsWatcher.updateWatchedRoots();
return clone;
}
@Override
public List<String> getInvalidRootUrls(OrderRootType type) {
if (myDisposed) return Collections.emptyList();
final List<VirtualFilePointer> pointers = myRoots.get(type).getList();
List<String> invalidPaths = null;
for (VirtualFilePointer pointer : pointers) {
if (!pointer.isValid()) {
if (invalidPaths == null) {
invalidPaths = new SmartList<String>();
}
invalidPaths.add(pointer.getUrl());
}
}
return invalidPaths == null ? Collections.<String>emptyList() : invalidPaths;
}
@Override
public void setProperties(LibraryProperties properties) {
LOG.assertTrue(isWritable());
myProperties = properties;
}
@Override
@NotNull
public RootProvider getRootProvider() {
return myRootProvider;
}
private Map<OrderRootType, VirtualFilePointerContainer> initRoots() {
Disposer.register(this, myPointersDisposable);
Map<OrderRootType, VirtualFilePointerContainer> result = new HashMap<OrderRootType, VirtualFilePointerContainer>(4);
for (OrderRootType rootType : getAllRootTypes()) {
result.put(rootType, VirtualFilePointerManager.getInstance().createContainer(myPointersDisposable));
}
return result;
}
@Override
public void readExternal(Element element) throws InvalidDataException {
readName(element);
readProperties(element);
readRoots(element);
myJarDirectories.readExternal(element);
myRootsWatcher.updateWatchedRoots();
}
private void readProperties(Element element) {
final String typeId = element.getAttributeValue(LIBRARY_TYPE_ATTR);
if (typeId == null) return;
myKind = (PersistentLibraryKind<?>) LibraryKind.findById(typeId);
if (myKind == null) return;
myProperties = myKind.createDefaultProperties();
final Element propertiesElement = element.getChild(PROPERTIES_ELEMENT);
if (propertiesElement != null) {
ComponentSerializationUtil.loadComponentState(myProperties, propertiesElement);
}
}
private void readName(Element element) {
myName = element.getAttributeValue(LIBRARY_NAME_ATTR);
}
private void readRoots(Element element) throws InvalidDataException {
for (OrderRootType rootType : getAllRootTypes()) {
final Element rootChild = element.getChild(rootType.name());
if (rootChild == null) {
continue;
}
VirtualFilePointerContainer roots = myRoots.get(rootType);
roots.readExternal(rootChild, ROOT_PATH_ELEMENT);
}
Element excludedRoot = element.getChild(EXCLUDED_ROOTS_TAG);
if (excludedRoot != null) {
getOrCreateExcludedRoots().readExternal(excludedRoot, ROOT_PATH_ELEMENT);
}
}
private VirtualFilePointerContainer getOrCreateExcludedRoots() {
if (myExcludedRoots == null) {
myExcludedRoots = VirtualFilePointerManager.getInstance().createContainer(myPointersDisposable);
}
return myExcludedRoots;
}
//TODO<rv> Remove the next two methods as a temporary solution. Sort in OrderRootType.
//
public static List<OrderRootType> sortRootTypes(Collection<OrderRootType> rootTypes) {
List<OrderRootType> allTypes = new ArrayList<OrderRootType>(rootTypes);
Collections.sort(allTypes, new Comparator<OrderRootType>() {
@Override
public int compare(@NotNull final OrderRootType o1, @NotNull final OrderRootType o2) {
return o1.name().compareToIgnoreCase(o2.name());
}
});
return allTypes;
}
@Override
public void writeExternal(Element rootElement) throws WriteExternalException {
checkDisposed();
Element element = new Element(ELEMENT);
if (myName != null) {
element.setAttribute(LIBRARY_NAME_ATTR, myName);
}
if (myKind != null) {
element.setAttribute(LIBRARY_TYPE_ATTR, myKind.getKindId());
final Object state = myProperties.getState();
if (state != null) {
final Element propertiesElement = XmlSerializer.serialize(state, SERIALIZATION_FILTERS);
if (propertiesElement != null && (!propertiesElement.getContent().isEmpty() || !propertiesElement.getAttributes().isEmpty())) {
element.addContent(propertiesElement.setName(PROPERTIES_ELEMENT));
}
}
}
ArrayList<OrderRootType> storableRootTypes = new ArrayList<OrderRootType>();
storableRootTypes.addAll(Arrays.asList(OrderRootType.getAllTypes()));
if (myKind != null) {
storableRootTypes.addAll(Arrays.asList(myKind.getAdditionalRootTypes()));
}
for (OrderRootType rootType : sortRootTypes(storableRootTypes)) {
final VirtualFilePointerContainer roots = myRoots.get(rootType);
if (roots.size() == 0 && rootType.skipWriteIfEmpty()) continue; //compatibility iml/ipr
final Element rootTypeElement = new Element(rootType.name());
roots.writeExternal(rootTypeElement, ROOT_PATH_ELEMENT);
element.addContent(rootTypeElement);
}
if (myExcludedRoots != null && myExcludedRoots.size() > 0) {
Element excluded = new Element(EXCLUDED_ROOTS_TAG);
myExcludedRoots.writeExternal(excluded, ROOT_PATH_ELEMENT);
element.addContent(excluded);
}
myJarDirectories.writeExternal(element);
rootElement.addContent(element);
}
private boolean isWritable() {
return mySource != null;
}
@Nullable
@Override
public PersistentLibraryKind<?> getKind() {
return myKind;
}
@Override
public void addExcludedRoot(@NotNull String url) {
VirtualFilePointerContainer roots = getOrCreateExcludedRoots();
if (roots.findByUrl(url) == null) {
roots.add(url);
}
}
@Override
public boolean removeExcludedRoot(@NotNull String url) {
if (myExcludedRoots != null) {
VirtualFilePointer pointer = myExcludedRoots.findByUrl(url);
if (pointer != null) {
myExcludedRoots.remove(pointer);
return true;
}
}
return false;
}
@NotNull
@Override
public String[] getExcludedRootUrls() {
return myExcludedRoots != null ? myExcludedRoots.getUrls() : ArrayUtil.EMPTY_STRING_ARRAY;
}
@NotNull
@Override
public VirtualFile[] getExcludedRoots() {
return myExcludedRoots != null ? myExcludedRoots.getFiles() : VirtualFile.EMPTY_ARRAY;
}
@Override
public LibraryProperties getProperties() {
return myProperties;
}
@Override
public void setKind(PersistentLibraryKind<?> kind) {
LOG.assertTrue(isWritable());
LOG.assertTrue(myKind == null || myKind == kind, "Library kind cannot be changed from " + myKind + " to " + kind);
myKind = kind;
}
@Override
public void addRoot(@NotNull String url, @NotNull OrderRootType rootType) {
checkDisposed();
LOG.assertTrue(isWritable());
final VirtualFilePointerContainer container = myRoots.get(rootType);
container.add(url);
}
@Override
public void addRoot(@NotNull VirtualFile file, @NotNull OrderRootType rootType) {
checkDisposed();
LOG.assertTrue(isWritable());
final VirtualFilePointerContainer container = myRoots.get(rootType);
container.add(file);
}
@Override
public void addJarDirectory(@NotNull final String url, final boolean recursive) {
addJarDirectory(url, recursive, JarDirectories.DEFAULT_JAR_DIRECTORY_TYPE);
}
@Override
public void addJarDirectory(@NotNull final VirtualFile file, final boolean recursive) {
addJarDirectory(file, recursive, JarDirectories.DEFAULT_JAR_DIRECTORY_TYPE);
}
@Override
public void addJarDirectory(@NotNull final String url, final boolean recursive, @NotNull OrderRootType rootType) {
checkDisposed();
LOG.assertTrue(isWritable());
final VirtualFilePointerContainer container = myRoots.get(rootType);
container.add(url);
myJarDirectories.add(rootType, url, recursive);
}
@Override
public void addJarDirectory(@NotNull final VirtualFile file, final boolean recursive, @NotNull OrderRootType rootType) {
checkDisposed();
LOG.assertTrue(isWritable());
final VirtualFilePointerContainer container = myRoots.get(rootType);
container.add(file);
myJarDirectories.add(rootType, file.getUrl(), recursive);
}
@Override
public boolean isJarDirectory(@NotNull final String url) {
return isJarDirectory(url, JarDirectories.DEFAULT_JAR_DIRECTORY_TYPE);
}
@Override
public boolean isJarDirectory(@NotNull final String url, @NotNull final OrderRootType rootType) {
return myJarDirectories.contains(rootType, url);
}
@Override
public boolean isValid(@NotNull final String url, @NotNull final OrderRootType rootType) {
final VirtualFilePointerContainer container = myRoots.get(rootType);
final VirtualFilePointer fp = container.findByUrl(url);
return fp != null && fp.isValid();
}
@Override
public boolean removeRoot(@NotNull String url, @NotNull OrderRootType rootType) {
checkDisposed();
LOG.assertTrue(isWritable());
final VirtualFilePointerContainer container = myRoots.get(rootType);
final VirtualFilePointer byUrl = container.findByUrl(url);
if (byUrl != null) {
container.remove(byUrl);
if (myExcludedRoots != null) {
for (String excludedRoot : myExcludedRoots.getUrls()) {
if (!isUnderRoots(excludedRoot)) {
VirtualFilePointer pointer = myExcludedRoots.findByUrl(excludedRoot);
if (pointer != null) {
myExcludedRoots.remove(pointer);
}
}
}
}
myJarDirectories.remove(rootType, url);
return true;
}
return false;
}
private boolean isUnderRoots(@NotNull String url) {
for (VirtualFilePointerContainer container : myRoots.values()) {
if (VfsUtilCore.isUnder(url, Arrays.asList(container.getUrls()))) {
return true;
}
}
return false;
}
@Override
public void moveRootUp(@NotNull String url, @NotNull OrderRootType rootType) {
checkDisposed();
LOG.assertTrue(isWritable());
final VirtualFilePointerContainer container = myRoots.get(rootType);
container.moveUp(url);
}
@Override
public void moveRootDown(@NotNull String url, @NotNull OrderRootType rootType) {
checkDisposed();
LOG.assertTrue(isWritable());
final VirtualFilePointerContainer container = myRoots.get(rootType);
container.moveDown(url);
}
@Override
public boolean isChanged() {
return !mySource.equals(this);
}
private boolean areRootsChanged(final LibraryImpl that) {
return !that.equals(this);
//final OrderRootType[] allTypes = OrderRootType.getAllTypes();
//for (OrderRootType type : allTypes) {
// final String[] urls = getUrls(type);
// final String[] thatUrls = that.getUrls(type);
// if (urls.length != thatUrls.length) {
// return true;
// }
// for (int idx = 0; idx < urls.length; idx++) {
// final String url = urls[idx];
// final String thatUrl = thatUrls[idx];
// if (!Comparing.equal(url, thatUrl)) {
// return true;
// }
// final Boolean jarDirRecursive = myJarDirectories.get(url);
// final Boolean sourceJarDirRecursive = that.myJarDirectories.get(thatUrl);
// if (jarDirRecursive == null ? sourceJarDirRecursive != null : !jarDirRecursive.equals(sourceJarDirRecursive)) {
// return true;
// }
// }
//}
//return false;
}
public Library getSource() {
return mySource;
}
@Override
public void commit() {
checkDisposed();
mySource.commit(this);
Disposer.dispose(this);
}
private void commit(@NotNull LibraryImpl fromModel) {
if (myLibraryTable != null) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
}
if (!Comparing.equal(fromModel.myName, myName)) {
myName = fromModel.myName;
if (myLibraryTable instanceof LibraryTableBase) {
((LibraryTableBase)myLibraryTable).fireLibraryRenamed(this);
}
}
myKind = fromModel.getKind();
myProperties = fromModel.myProperties;
if (areRootsChanged(fromModel)) {
disposeMyPointers();
copyRootsFrom(fromModel);
myJarDirectories.copyFrom(fromModel.myJarDirectories);
myRootsWatcher.updateWatchedRoots();
myRootProvider.fireRootSetChanged();
}
}
private void copyRootsFrom(LibraryImpl fromModel) {
Map<OrderRootType, VirtualFilePointerContainer> clonedRoots = ContainerUtil.newHashMap();
for (Map.Entry<OrderRootType, VirtualFilePointerContainer> entry : fromModel.myRoots.entrySet()) {
OrderRootType rootType = entry.getKey();
VirtualFilePointerContainer container = entry.getValue();
VirtualFilePointerContainer clone = container.clone(myPointersDisposable);
clonedRoots.put(rootType, clone);
}
myRoots.clear();
myRoots.putAll(clonedRoots);
VirtualFilePointerContainer excludedRoots = fromModel.myExcludedRoots;
myExcludedRoots = excludedRoots != null ? excludedRoots.clone(myPointersDisposable) : null;
}
private void disposeMyPointers() {
for (VirtualFilePointerContainer container : new THashSet<VirtualFilePointerContainer>(myRoots.values())) {
container.killAll();
}
if (myExcludedRoots != null) {
myExcludedRoots.killAll();
}
Disposer.dispose(myPointersDisposable);
Disposer.register(this, myPointersDisposable);
}
private class MyRootProviderImpl extends RootProviderBaseImpl {
@Override
@NotNull
public String[] getUrls(@NotNull OrderRootType rootType) {
Set<String> originalUrls = new LinkedHashSet<String>(Arrays.asList(LibraryImpl.this.getUrls(rootType)));
for (VirtualFile file : getFiles(rootType)) { // Add those expanded with jar directories.
originalUrls.add(file.getUrl());
}
return ArrayUtil.toStringArray(originalUrls);
}
@Override
@NotNull
public VirtualFile[] getFiles(@NotNull final OrderRootType rootType) {
return LibraryImpl.this.getFiles(rootType);
}
}
@Override
public LibraryTable getTable() {
return myLibraryTable;
}
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final LibraryImpl library = (LibraryImpl)o;
if (!myJarDirectories.equals(library.myJarDirectories)) return false;
if (myName != null ? !myName.equals(library.myName) : library.myName != null) return false;
if (myRoots != null ? !myRoots.equals(library.myRoots) : library.myRoots != null) return false;
if (myKind != null ? !myKind.equals(library.myKind) : library.myKind != null) return false;
if (myProperties != null ? !myProperties.equals(library.myProperties) : library.myProperties != null) return false;
if (!Comparing.equal(myExcludedRoots, library.myExcludedRoots)) return false;
return true;
}
public int hashCode() {
int result = myName != null ? myName.hashCode() : 0;
result = 31 * result + (myRoots != null ? myRoots.hashCode() : 0);
result = 31 * result + myJarDirectories.hashCode();
return result;
}
@NonNls
@Override
public String toString() {
return "Library: name:" + myName + "; jars:" + myJarDirectories + "; roots:" + myRoots.values();
}
@Nullable("will return non-null value only for module level libraries")
public Module getModule() {
return myRootModel == null ? null : myRootModel.getModule();
}
}