| /* |
| * 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()); |
| } |
| } |