package org.rubypeople.rdt.internal.core;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.AssertionFailedException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.rubypeople.rdt.core.ILoadpathAttribute;
import org.rubypeople.rdt.core.ILoadpathEntry;
import org.rubypeople.rdt.core.IRubyModelStatus;
import org.rubypeople.rdt.core.IRubyProject;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.internal.core.util.CharOperation;
import org.rubypeople.rdt.internal.core.util.Messages;
import org.w3c.dom.DOMException;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
public class LoadpathEntry implements ILoadpathEntry {
public static final String TAG_LOADPATH = "loadpath"; //$NON-NLS-1$
public static final String TAG_LOADPATHENTRY = "pathentry"; //$NON-NLS-1$
public static final String TAG_KIND = "type"; //$NON-NLS-1$
public static final String TAG_PATH = "path"; //$NON-NLS-1$
public static final String TAG_EXPORTED = "exported"; //$NON-NLS-1$
public static final String TAG_INCLUDING = "including"; //$NON-NLS-1$
public static final String TAG_EXCLUDING = "excluding"; //$NON-NLS-1$
public static final String TAG_ATTRIBUTES = "attributes"; //$NON-NLS-1$
public static final String TAG_ATTRIBUTE = "attribute"; //$NON-NLS-1$
public static final String TAG_ATTRIBUTE_NAME = "name"; //$NON-NLS-1$
public static final String TAG_ATTRIBUTE_VALUE = "value"; //$NON-NLS-1$
static class UnknownXmlElements {
String[] attributes;
ArrayList children;
}
private static final String TYPE_PROJECT = "project";
private String rootID;
private int entryKind;
private IPath path;
/**
* Patterns allowing to include/exclude portions of the resource tree
* denoted by this entry path.
*/
private IPath[] inclusionPatterns;
private char[][] fullInclusionPatternChars;
private IPath[] exclusionPatterns;
private char[][] fullExclusionPatternChars;
private final static char[][] UNINIT_PATTERNS = new char[][] { "Non-initialized yet".toCharArray()}; //$NON-NLS-1$
/*
* Default extra attributes
*/
public final static ILoadpathAttribute[] NO_EXTRA_ATTRIBUTES = {};
/*
* Default inclusion pattern set
*/
public final static IPath[] INCLUDE_ALL = {};
/*
* Default exclusion pattern set
*/
public final static IPath[] EXCLUDE_NONE = {};
private IProject project;
/**
* The export flag
*/
private boolean isExported;
/*
* The extra attributes
*/
ILoadpathAttribute[] extraAttributes;
public LoadpathEntry(int entryKind, IPath path, IPath[] inclusionPatterns, IPath[] exclusionPatterns, ILoadpathAttribute[] extraAttributes, boolean isExported) {
this.path = path;
this.entryKind = entryKind;
this.inclusionPatterns = inclusionPatterns;
this.exclusionPatterns = exclusionPatterns;
this.extraAttributes = extraAttributes;
if (inclusionPatterns != INCLUDE_ALL && inclusionPatterns.length > 0) {
this.fullInclusionPatternChars = UNINIT_PATTERNS;
}
if (exclusionPatterns.length > 0) {
this.fullExclusionPatternChars = UNINIT_PATTERNS;
}
this.isExported = isExported;
}
public IPath getPath() {
return path;
}
public int getEntryKind() {
return this.entryKind;
}
/**
* Returns a <code>String</code> for the kind of a class path entry.
*/
static String kindToString(int kind) {
switch (kind) {
case ILoadpathEntry.CPE_PROJECT:
return TYPE_PROJECT; //$NON-NLS-1$
case ILoadpathEntry.CPE_SOURCE:
return "src"; //$NON-NLS-1$
case ILoadpathEntry.CPE_LIBRARY:
return "lib"; //$NON-NLS-1$
case ILoadpathEntry.CPE_VARIABLE:
return "var"; //$NON-NLS-1$
case ILoadpathEntry.CPE_CONTAINER:
return "con"; //$NON-NLS-1$
default:
return "unknown"; //$NON-NLS-1$
}
}
public String toXML() {
StringBuffer buffer = new StringBuffer();
buffer.append("<pathentry type=\"");
buffer.append(LoadpathEntry.kindToString(entryKind) + "\" ");
buffer.append("path=\"" + getPath() + "\"/>");
return buffer.toString();
}
/**
* Answers an ID which is used to distinguish entries during package
* fragment root computations
*/
public String rootID() {
if (this.rootID == null) {
switch (this.entryKind) {
case ILoadpathEntry.CPE_LIBRARY:
this.rootID = "[LIB]" + this.path; //$NON-NLS-1$
break;
case ILoadpathEntry.CPE_PROJECT:
this.rootID = "[PRJ]" + this.path; //$NON-NLS-1$
break;
case ILoadpathEntry.CPE_SOURCE:
this.rootID = "[SRC]" + this.path; //$NON-NLS-1$
break;
case ILoadpathEntry.CPE_VARIABLE:
this.rootID = "[VAR]" + this.path; //$NON-NLS-1$
break;
case ILoadpathEntry.CPE_CONTAINER:
this.rootID = "[CON]" + this.path; //$NON-NLS-1$
break;
default:
this.rootID = ""; //$NON-NLS-1$
break;
}
}
return this.rootID;
}
/*
* Returns a char based representation of the exclusions patterns full path.
*/
public char[][] fullExclusionPatternChars() {
if (this.fullExclusionPatternChars == UNINIT_PATTERNS) {
int length = this.exclusionPatterns.length;
this.fullExclusionPatternChars = new char[length][];
IPath prefixPath = this.path.removeTrailingSeparator();
for (int i = 0; i < length; i++) {
this.fullExclusionPatternChars[i] = prefixPath.append(this.exclusionPatterns[i]).toString().toCharArray();
}
}
return this.fullExclusionPatternChars;
}
/*
* Returns a char based representation of the exclusions patterns full path.
*/
public char[][] fullInclusionPatternChars() {
if (this.fullInclusionPatternChars == UNINIT_PATTERNS) {
int length = this.inclusionPatterns.length;
this.fullInclusionPatternChars = new char[length][];
IPath prefixPath = this.path.removeTrailingSeparator();
for (int i = 0; i < length; i++) {
this.fullInclusionPatternChars[i] = prefixPath.append(this.inclusionPatterns[i]).toString().toCharArray();
}
}
return this.fullInclusionPatternChars;
}
/**
* @see ILoadpathEntry#isExported()
*/
public boolean isExported() {
return this.isExported;
}
/* (non-Javadoc)
* @see org.rubypeople.rdt.core.ILoadpathEntry#getExclusionPatterns()
*/
public IPath[] getExclusionPatterns() {
return exclusionPatterns;
}
/* (non-Javadoc)
* @see org.rubypeople.rdt.core.ILoadpathEntry#getInclusionPatterns()
*/
public IPath[] getInclusionPatterns() {
return inclusionPatterns;
}
public LoadpathEntry combineWith(LoadpathEntry referringEntry) {
if (referringEntry == null) return this;
if (referringEntry.isExported() ) {
return new LoadpathEntry(
getEntryKind(),
getPath(),
this.inclusionPatterns,
this.exclusionPatterns,
this.extraAttributes,
referringEntry.isExported() || this.isExported); // duplicate container entry for tagging it as exported
}
// no need to clone
return this;
}
public static ILoadpathEntry elementDecode(Element element, IRubyProject project, Map unknownElements) {
IPath projectPath = project.getProject().getFullPath();
NamedNodeMap attributes = element.getAttributes();
NodeList children = element.getChildNodes();
boolean[] foundChildren = new boolean[children.getLength()];
String kindAttr = removeAttribute(TAG_KIND, attributes);
String pathAttr = removeAttribute(TAG_PATH, attributes);
// ensure path is absolute
IPath path = new Path(pathAttr);
int kind = kindFromString(kindAttr);
if (kind != ILoadpathEntry.CPE_VARIABLE && kind != ILoadpathEntry.CPE_CONTAINER && !path.isAbsolute()) {
path = projectPath.append(path);
}
// exported flag (optional)
boolean isExported = removeAttribute(TAG_EXPORTED, attributes).equals("true"); //$NON-NLS-1$
// inclusion patterns (optional)
IPath[] inclusionPatterns = decodePatterns(attributes, TAG_INCLUDING);
if (inclusionPatterns == null) inclusionPatterns = INCLUDE_ALL;
// exclusion patterns (optional)
IPath[] exclusionPatterns = decodePatterns(attributes, TAG_EXCLUDING);
if (exclusionPatterns == null) exclusionPatterns = EXCLUDE_NONE;
// extra attributes (optional)
NodeList attributeList = getChildAttributes(TAG_ATTRIBUTES, children, foundChildren);
ILoadpathAttribute[] extraAttributes = decodeExtraAttributes(attributeList);
String[] unknownAttributes = null;
ArrayList unknownChildren = null;
if (unknownElements != null) {
// unknown attributes
int unknownAttributeLength = attributes.getLength();
if (unknownAttributeLength != 0) {
unknownAttributes = new String[unknownAttributeLength*2];
for (int i = 0; i < unknownAttributeLength; i++) {
Node attribute = attributes.item(i);
unknownAttributes[i*2] = attribute.getNodeName();
unknownAttributes[i*2 + 1] = attribute.getNodeValue();
}
}
// unknown children
for (int i = 0, length = foundChildren.length; i < length; i++) {
if (!foundChildren[i]) {
Node node = children.item(i);
if (node.getNodeType() != Node.ELEMENT_NODE) continue;
if (unknownChildren == null)
unknownChildren = new ArrayList();
StringBuffer buffer = new StringBuffer();
decodeUnknownNode(node, buffer, project);
unknownChildren.add(buffer.toString());
}
}
}
// recreate the CP entry
ILoadpathEntry entry = null;
switch (kind) {
case ILoadpathEntry.CPE_PROJECT :
entry = new LoadpathEntry(
ILoadpathEntry.CPE_PROJECT,
path,
LoadpathEntry.INCLUDE_ALL, // inclusion patterns
LoadpathEntry.EXCLUDE_NONE, // exclusion patterns
extraAttributes,
isExported);
break;
case ILoadpathEntry.CPE_LIBRARY :
entry = RubyCore.newLibraryEntry(
path,
extraAttributes,
isExported);
break;
case ILoadpathEntry.CPE_SOURCE :
// must be an entry in this project or specify another project
String projSegment = path.segment(0);
if (projSegment != null && projSegment.equals(project.getElementName())) { // this project
entry = RubyCore.newSourceEntry(path, inclusionPatterns, exclusionPatterns, extraAttributes);
} else {
if (path.segmentCount() == 1) {
// another project
entry = RubyCore.newProjectEntry(
path,
extraAttributes,
isExported);
} else {
// an invalid source folder
entry = RubyCore.newSourceEntry(path, inclusionPatterns, exclusionPatterns, extraAttributes);
}
}
break;
case ILoadpathEntry.CPE_VARIABLE :
entry = RubyCore.newVariableEntry(
path,
extraAttributes,
isExported);
break;
case ILoadpathEntry.CPE_CONTAINER :
entry = RubyCore.newContainerEntry(
path,
extraAttributes,
isExported);
break;
default :
throw new AssertionFailedException(Messages.bind(Messages.classpath_unknownKind, kindAttr));
}
if (unknownAttributes != null || unknownChildren != null) {
UnknownXmlElements unknownXmlElements = new UnknownXmlElements();
unknownXmlElements.attributes = unknownAttributes;
unknownXmlElements.children = unknownChildren;
unknownElements.put(path, unknownXmlElements);
}
return entry;
}
public static NodeList getChildAttributes(String childName, NodeList children, boolean[] foundChildren) {
for (int i = 0, length = foundChildren.length; i < length; i++) {
Node node = children.item(i);
if (childName.equals(node.getNodeName())) {
foundChildren[i] = true;
return node.getChildNodes();
}
}
return null;
}
static ILoadpathAttribute[] decodeExtraAttributes(NodeList attributes) {
if (attributes == null) return NO_EXTRA_ATTRIBUTES;
int length = attributes.getLength();
if (length == 0) return NO_EXTRA_ATTRIBUTES;
ILoadpathAttribute[] result = new ILoadpathAttribute[length];
int index = 0;
for (int i = 0; i < length; ++i) {
Node node = attributes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element attribute = (Element)node;
String name = attribute.getAttribute(TAG_ATTRIBUTE_NAME);
if (name == null) continue;
String value = attribute.getAttribute(TAG_ATTRIBUTE_VALUE);
if (value == null) continue;
result[index++] = new LoadpathAttribute(name, value);
}
}
if (index != length)
System.arraycopy(result, 0, result = new ILoadpathAttribute[index], 0, index);
return result;
}
private static void decodeUnknownNode(Node node, StringBuffer buffer, IRubyProject project) {
ByteArrayOutputStream s = new ByteArrayOutputStream();
OutputStreamWriter writer;
try {
writer = new OutputStreamWriter(s, "UTF8"); //$NON-NLS-1$
XMLWriter xmlWriter = new XMLWriter(writer, project, false/*don't print XML version*/);
decodeUnknownNode(node, xmlWriter, true/*insert new line*/);
xmlWriter.flush();
xmlWriter.close();
buffer.append(s.toString("UTF8")); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
// ignore (UTF8 is always supported)
}
}
private static void decodeUnknownNode(Node node, XMLWriter xmlWriter, boolean insertNewLine) {
switch (node.getNodeType()) {
case Node.ELEMENT_NODE:
NamedNodeMap attributes;
HashMap parameters = null;
if ((attributes = node.getAttributes()) != null) {
int length = attributes.getLength();
if (length > 0) {
parameters = new HashMap();
for (int i = 0; i < length; i++) {
Node attribute = attributes.item(i);
parameters.put(attribute.getNodeName(), attribute.getNodeValue());
}
}
}
NodeList children = node.getChildNodes();
int childrenLength = children.getLength();
String nodeName = node.getNodeName();
xmlWriter.printTag(nodeName, parameters, false/*don't insert tab*/, false/*don't insert new line*/, childrenLength == 0/*close tag if no children*/);
if (childrenLength > 0) {
for (int i = 0; i < childrenLength; i++) {
decodeUnknownNode(children.item(i), xmlWriter, false/*don't insert new line*/);
}
xmlWriter.endTag(nodeName, false/*don't insert tab*/, insertNewLine);
}
break;
case Node.TEXT_NODE:
String data = ((Text) node).getData();
xmlWriter.printString(data, false/*don't insert tab*/, false/*don't insert new line*/);
break;
}
}
/**
* Decode some element tag containing a sequence of patterns into IPath[]
*/
private static IPath[] decodePatterns(NamedNodeMap nodeMap, String tag) {
String sequence = removeAttribute(tag, nodeMap);
if (!sequence.equals("")) { //$NON-NLS-1$
char[][] patterns = CharOperation.splitOn('|', sequence.toCharArray());
int patternCount;
if ((patternCount = patterns.length) > 0) {
IPath[] paths = new IPath[patternCount];
int index = 0;
for (int j = 0; j < patternCount; j++) {
char[] pattern = patterns[j];
if (pattern.length == 0) continue; // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=105581
paths[index++] = new Path(new String(pattern));
}
if (index < patternCount)
System.arraycopy(paths, 0, paths = new IPath[index], 0, index);
return paths;
}
}
return null;
}
/**
* Returns the kind of a <code>SourceFoldertRoot</code> from its <code>String</code> form.
*/
static int kindFromString(String kindStr) {
if (kindStr.equalsIgnoreCase("project")) //$NON-NLS-1$
return ILoadpathEntry.CPE_PROJECT;
if (kindStr.equalsIgnoreCase("var")) //$NON-NLS-1$
return ILoadpathEntry.CPE_VARIABLE;
if (kindStr.equalsIgnoreCase("con")) //$NON-NLS-1$
return ILoadpathEntry.CPE_CONTAINER;
if (kindStr.equalsIgnoreCase("src")) //$NON-NLS-1$
return ILoadpathEntry.CPE_SOURCE;
if (kindStr.equalsIgnoreCase("lib")) //$NON-NLS-1$
return ILoadpathEntry.CPE_LIBRARY;
return -1;
}
private static String removeAttribute(String nodeName, NamedNodeMap nodeMap) {
Node node = removeNode(nodeName, nodeMap);
if (node == null)
return ""; // //$NON-NLS-1$
return node.getNodeValue();
}
private static Node removeNode(String nodeName, NamedNodeMap nodeMap) {
try {
return nodeMap.removeNamedItem(nodeName);
} catch (DOMException e) {
if (e.code != DOMException.NOT_FOUND_ERR)
throw e;
return null;
}
}
public boolean isOptional() {
for (int i = 0, length = this.extraAttributes.length; i < length; i++) {
ILoadpathAttribute attribute = this.extraAttributes[i];
if (ILoadpathAttribute.OPTIONAL.equals(attribute.getName()) && "true".equals(attribute.getValue())) //$NON-NLS-1$
return true;
}
return false;
}
public static IRubyModelStatus validateLoadpathEntry(IRubyProject project,
ILoadpathEntry rawEntry, boolean b, boolean c) {
// TODO Actually do some checking of the entry
return RubyModelStatus.VERIFIED_OK;
}
public static IRubyModelStatus validateLoadpath(IRubyProject project,
ILoadpathEntry[] resolvedPath, IPath projectOutputLocation) {
// FIXME Remove outputLocation
// TODO Actually do some checking of the loadpath
return RubyModelStatus.VERIFIED_OK;
}
/**
* Returns the XML encoding of the class path.
*/
public void elementEncode(XMLWriter writer, IPath projectPath, boolean indent, boolean newLine, Map unknownElements) {
HashMap parameters = new HashMap();
parameters.put(TAG_KIND, LoadpathEntry.kindToString(this.entryKind));
IPath xmlPath = this.path;
if (this.entryKind != ILoadpathEntry.CPE_VARIABLE && this.entryKind != ILoadpathEntry.CPE_CONTAINER) {
// translate to project relative from absolute (unless a device path)
if (xmlPath.isAbsolute()) {
if (projectPath != null && projectPath.isPrefixOf(xmlPath)) {
if (xmlPath.segment(0).equals(projectPath.segment(0))) {
xmlPath = xmlPath.removeFirstSegments(1);
xmlPath = xmlPath.makeRelative();
} else {
xmlPath = xmlPath.makeAbsolute();
}
}
}
}
parameters.put(TAG_PATH, String.valueOf(xmlPath));
if (this.isExported) {
parameters.put(TAG_EXPORTED, "true");//$NON-NLS-1$
}
encodePatterns(this.inclusionPatterns, TAG_INCLUDING, parameters);
encodePatterns(this.exclusionPatterns, TAG_EXCLUDING, parameters);
// unknown attributes
UnknownXmlElements unknownXmlElements = unknownElements == null ? null : (UnknownXmlElements) unknownElements.get(this.path);
String[] unknownAttributes;
if (unknownXmlElements != null && (unknownAttributes = unknownXmlElements.attributes) != null)
for (int i = 0, length = unknownAttributes.length; i < length; i+=2) {
String tagName = unknownAttributes[i];
String tagValue = unknownAttributes[i+1];
parameters.put(tagName, tagValue);
}
boolean hasExtraAttributes = this.extraAttributes.length != 0;
ArrayList unknownChildren = unknownXmlElements != null ? unknownXmlElements.children : null;
boolean hasUnknownChildren = unknownChildren != null;
writer.printTag(
TAG_LOADPATHENTRY,
parameters,
indent,
newLine,
!hasUnknownChildren/*close tag if no unknown children*/);
if (hasExtraAttributes)
encodeExtraAttributes(writer, indent, newLine);
if (hasUnknownChildren) {
encodeUnknownChildren(writer, indent, newLine, unknownChildren);
if (hasExtraAttributes || hasUnknownChildren)
writer.endTag(TAG_LOADPATHENTRY, indent, true/*insert new line*/);
}
}
void encodeExtraAttributes(XMLWriter writer, boolean indent, boolean newLine) {
writer.startTag(TAG_ATTRIBUTES, indent);
for (int i = 0; i < this.extraAttributes.length; i++) {
ILoadpathAttribute attribute = this.extraAttributes[i];
HashMap parameters = new HashMap();
parameters.put(TAG_ATTRIBUTE_NAME, attribute.getName());
parameters.put(TAG_ATTRIBUTE_VALUE, attribute.getValue());
writer.printTag(TAG_ATTRIBUTE, parameters, indent, newLine, true);
}
writer.endTag(TAG_ATTRIBUTES, indent, true/*insert new line*/);
}
/**
* Encode some patterns into XML parameter tag
*/
private static void encodePatterns(IPath[] patterns, String tag, Map parameters) {
if (patterns != null && patterns.length > 0) {
StringBuffer rule = new StringBuffer(10);
for (int i = 0, max = patterns.length; i < max; i++){
if (i > 0) rule.append('|');
rule.append(patterns[i]);
}
parameters.put(tag, String.valueOf(rule));
}
}
private void encodeUnknownChildren(XMLWriter writer, boolean indent, boolean newLine, ArrayList unknownChildren) {
for (int i = 0, length = unknownChildren.size(); i < length; i++) {
String child = (String) unknownChildren.get(i);
writer.printString(child, indent, false/*don't insert new line*/);
}
}
public ILoadpathAttribute[] getExtraAttributes() {
return extraAttributes;
}
/**
* Returns true if the given object is a classpath entry
* with equivalent attributes.
*/
public boolean equals(Object object) {
if (this == object)
return true;
if (object instanceof LoadpathEntry) {
LoadpathEntry otherEntry = (LoadpathEntry) object;
if (this.entryKind != otherEntry.getEntryKind())
return false;
if (this.isExported != otherEntry.isExported())
return false;
if (!this.path.equals(otherEntry.getPath()))
return false;
if (!equalPatterns(this.inclusionPatterns, otherEntry.getInclusionPatterns()))
return false;
if (!equalPatterns(this.exclusionPatterns, otherEntry.getExclusionPatterns()))
return false;
if (!equalAttributes(this.extraAttributes, otherEntry.getExtraAttributes()))
return false;
return true;
} else {
return false;
}
}
private static boolean equalAttributes(ILoadpathAttribute[] firstAttributes, ILoadpathAttribute[] secondAttributes) {
if (firstAttributes != secondAttributes){
if (firstAttributes == null) return false;
int length = firstAttributes.length;
if (secondAttributes == null || secondAttributes.length != length)
return false;
for (int i = 0; i < length; i++) {
if (!firstAttributes[i].equals(secondAttributes[i]))
return false;
}
}
return true;
}
private static boolean equalPatterns(IPath[] firstPatterns, IPath[] secondPatterns) {
if (firstPatterns != secondPatterns){
if (firstPatterns == null) return false;
int length = firstPatterns.length;
if (secondPatterns == null || secondPatterns.length != length)
return false;
for (int i = 0; i < length; i++) {
// compare toStrings instead of IPaths
// since IPath.equals is specified to ignore trailing separators
if (!firstPatterns[i].toString().equals(secondPatterns[i].toString()))
return false;
}
}
return true;
}
/**
* Returns the hash code for this classpath entry
*/
public int hashCode() {
return this.path.hashCode();
}
public String toString() {
return getPath().toPortableString();
}
}