blob: 10fc11acbf5fa0e929674b72f93dfc7adda03802 [file] [log] [blame]
/*
* Copyright 2000-2013 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.externalSystem.model;
import com.intellij.openapi.util.io.StreamUtil;
import com.intellij.util.containers.ContainerUtilRt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.*;
/**
* This class provides a generic graph infrastructure with ability to store particular data. The main purpose is to
* allow easy extensible data domain construction.
* <p/>
* Example: we might want to describe project model like 'project' which has multiple 'module' children where every
* 'module' has a collection of child 'content root' and dependencies nodes etc. When that is done, plugins can easily
* enhance any project. For example, particular framework can add facet settings as one more 'project' node's child.
* <p/>
* Not thread-safe.
*
* @author Denis Zhdanov
* @since 4/12/13 11:53 AM
*/
public class DataNode<T> implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull private final List<DataNode<?>> myChildren = ContainerUtilRt.newArrayList();
@NotNull private final Key<T> myKey;
private transient T myData;
private byte[] myRawData;
@Nullable private final DataNode<?> myParent;
public DataNode(@NotNull Key<T> key, @NotNull T data, @Nullable DataNode<?> parent) {
myKey = key;
myData = data;
myParent = parent;
}
@Nullable
public DataNode<?> getParent() {
return myParent;
}
@NotNull
public <T> DataNode<T> createChild(@NotNull Key<T> key, @NotNull T data) {
DataNode<T> result = new DataNode<T>(key, data, this);
myChildren.add(result);
return result;
}
@NotNull
public <T> DataNode<T> createOrReplaceChild(@NotNull Key<T> key, @NotNull T data) {
for (Iterator<DataNode<?>> iterator = myChildren.iterator(); iterator.hasNext(); ) {
DataNode<?> child = iterator.next();
if (child.getKey().equals(key)) {
iterator.remove();
break;
}
}
return createChild(key, data);
}
@NotNull
public Key<T> getKey() {
return myKey;
}
@NotNull
public T getData() {
if (myData == null) {
prepareData(getClass().getClassLoader(), Thread.currentThread().getContextClassLoader());
}
return myData;
}
/**
* This class is a generic holder for any kind of project data. That project data might originate from different locations, e.g.
* core ide plugins, non-core ide plugins, third-party plugins etc. That means that when a service from a core plugin needs to
* unmarshall {@link DataNode} object, its content should not be unmarshalled as well because its class might be unavailable here.
* <p/>
* That's why the content is delivered as a raw byte array and this method allows to build actual java object from it using
* the right class loader.
* <p/>
* This method is a no-op if the content is already built.
*
* @param loaders class loaders which are assumed to be able to build object of the target content class
*/
@SuppressWarnings({"unchecked", "IOResourceOpenedButNotSafelyClosed"})
public void prepareData(@NotNull final ClassLoader ... loaders) {
if (myData != null) {
return;
}
ObjectInputStream oIn = null;
try {
oIn = new ObjectInputStream(new ByteArrayInputStream(myRawData)) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
String name = desc.getName();
for (ClassLoader loader : loaders) {
try {
return Class.forName(name, false, loader);
}
catch (ClassNotFoundException e) {
// Ignore
}
}
return super.resolveClass(desc);
}
@Override
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
for (ClassLoader loader : loaders) {
try {
return doResolveProxyClass(interfaces, loader);
}
catch (ClassNotFoundException e) {
// Ignore
}
}
return super.resolveProxyClass(interfaces);
}
private Class<?> doResolveProxyClass(@NotNull String[] interfaces, @NotNull ClassLoader loader) throws ClassNotFoundException {
ClassLoader nonPublicLoader = null;
boolean hasNonPublicInterface = false;
// define proxy in class loader of non-public interface(s), if any
Class[] classObjs = new Class[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
Class cl = Class.forName(interfaces[i], false, loader);
if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
if (hasNonPublicInterface) {
if (nonPublicLoader != cl.getClassLoader()) {
throw new IllegalAccessError(
"conflicting non-public interface class loaders");
}
} else {
nonPublicLoader = cl.getClassLoader();
hasNonPublicInterface = true;
}
}
classObjs[i] = cl;
}
try {
return Proxy.getProxyClass(hasNonPublicInterface ? nonPublicLoader : loader, classObjs);
}
catch (IllegalArgumentException e) {
throw new ClassNotFoundException(null, e);
}
}
};
myData = (T)oIn.readObject();
myRawData = null;
}
catch (IOException e) {
throw new IllegalStateException(
String.format("Can't deserialize target data of key '%s'. Given class loaders: %s", myKey, Arrays.toString(loaders)),
e
);
}
catch (ClassNotFoundException e) {
throw new IllegalStateException(
String.format("Can't deserialize target data of key '%s'. Given class loaders: %s", myKey, Arrays.toString(loaders)),
e
);
}
finally {
StreamUtil.closeStream(oIn);
}
}
/**
* Allows to retrieve data stored for the given key at the current node or any of its parents.
*
* @param key target data's key
* @param <T> target data type
* @return data stored for the current key and available via the current node (if any)
*/
@SuppressWarnings("unchecked")
@Nullable
public <T> T getData(@NotNull Key<T> key) {
if (myKey.equals(key)) {
return (T)myData;
}
for (DataNode<?> p = myParent; p != null; p = p.myParent) {
if (p.myKey.equals(key)) {
return (T)p.myData;
}
}
return null;
}
@SuppressWarnings("unchecked")
@Nullable
public <T> DataNode<T> getDataNode(@NotNull Key<T> key) {
if (myKey.equals(key)) {
return (DataNode<T>)this;
}
for (DataNode<?> p = myParent; p != null; p = p.myParent) {
if (p.myKey.equals(key)) {
return (DataNode<T>)p;
}
}
return null;
}
public void addChild(@NotNull DataNode<?> child) {
myChildren.add(child);
}
@NotNull
public Collection<DataNode<?>> getChildren() {
return myChildren;
}
private void writeObject(ObjectOutputStream out) throws IOException {
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
ObjectOutputStream oOut = new ObjectOutputStream(bOut);
try {
oOut.writeObject(myData);
}
finally {
oOut.close();
}
myRawData = bOut.toByteArray();
out.defaultWriteObject();
}
@Override
public int hashCode() {
int result = myChildren.hashCode();
result = 31 * result + myKey.hashCode();
result = 31 * result + getData().hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DataNode node = (DataNode)o;
if (!myChildren.equals(node.myChildren)) return false;
if (!getData().equals(node.getData())) return false;
if (!myKey.equals(node.myKey)) return false;
return true;
}
@Override
public String toString() {
return String.format("%s: %s", myKey, getData());
}
}