/**
* Copyright (C) 2010-2014 Morgner UG (haftungsbeschränkt)
*
* This file is part of Structr <http://structr.org>.
*
* Structr is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Structr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Structr. If not, see <http://www.gnu.org/licenses/>.
*/
package org.structr.core.entity;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.codec.digest.DigestUtils;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.index.Index;
import org.structr.common.AccessControllable;
import org.structr.common.GraphObjectComparator;
import org.structr.common.Permission;
import org.structr.common.PropertyView;
import org.structr.common.SecurityContext;
import org.structr.common.ValidationHelper;
import org.structr.common.View;
import org.structr.common.error.ErrorBuffer;
import org.structr.common.error.FrameworkException;
import org.structr.common.error.NullArgumentToken;
import org.structr.common.error.ReadOnlyPropertyToken;
import org.structr.core.GraphObject;
import org.structr.core.IterableAdapter;
import org.structr.core.Ownership;
import org.structr.core.Services;
import org.structr.core.app.StructrApp;
import org.structr.core.converter.PropertyConverter;
import org.structr.core.entity.relationship.PrincipalOwnsNode;
import org.structr.core.graph.NodeInterface;
import org.structr.core.graph.NodeRelationshipStatisticsCommand;
import org.structr.core.graph.NodeService;
import org.structr.core.graph.RelationshipFactory;
import org.structr.core.property.PropertyKey;
import org.structr.core.property.PropertyMap;
import org.structr.schema.SchemaHelper;
import org.structr.schema.action.ActionContext;
//~--- classes ----------------------------------------------------------------
/**
* Abstract base class for all node entities in structr.
*
* @author Axel Morgner
* @author Christian Morgner
*/
public abstract class AbstractNode implements NodeInterface, AccessControllable {
private static final Map<Class, Object> relationshipTemplateInstanceCache = new LinkedHashMap<>();
private static final Logger logger = Logger.getLogger(AbstractNode.class.getName());
public static final View defaultView = new View(AbstractNode.class, PropertyView.Public, id, type);
public static final View uiView = new View(AbstractNode.class, PropertyView.Ui,
id, name, owner, type, createdBy, deleted, hidden, createdDate, lastModifiedDate, visibleToPublicUsers, visibleToAuthenticatedUsers, visibilityStartDate, visibilityEndDate
);
private boolean readOnlyPropertiesUnlocked = false;
protected Class entityType = null;
protected Principal cachedOwnerNode = null;
protected SecurityContext securityContext = null;
protected String cachedUuid = null;
protected Node dbNode = null;
//~--- constructors ---------------------------------------------------
public AbstractNode() {}
public AbstractNode(SecurityContext securityContext, final Node dbNode, final Class entityType) {
init(securityContext, dbNode, entityType);
}
//~--- methods --------------------------------------------------------
@Override
public void onNodeCreation() {
}
@Override
public void onNodeInstantiation() {
}
@Override
public void onNodeDeletion() {
}
@Override
public final void init(final SecurityContext securityContext, final Node dbNode, final Class entityType) {
this.dbNode = dbNode;
this.entityType = entityType;
this.securityContext = securityContext;
}
@Override
public void setSecurityContext(SecurityContext securityContext) {
this.securityContext = securityContext;
}
@Override
public SecurityContext getSecurityContext() {
return securityContext;
}
@Override
public boolean equals(final Object o) {
if (o == null) {
return false;
}
if (!(o instanceof AbstractNode)) {
return false;
}
return (Integer.valueOf(this.hashCode()).equals(o.hashCode()));
}
@Override
public int hashCode() {
if (this.dbNode == null) {
return (super.hashCode());
}
return Long.valueOf(dbNode.getId()).hashCode();
}
@Override
public int compareTo(final NodeInterface node) {
if(node == null) {
return -1;
}
String name = getName();
if(name == null) {
return -1;
}
String nodeName = node.getName();
if(nodeName == null) {
return -1;
}
return name.compareTo(nodeName);
}
/**
* Implement standard toString() method
*/
@Override
public String toString() {
return getUuid();
}
/**
* Can be used to permit the setting of a read-only
* property once. The lock will be restored automatically
* after the next setProperty operation. This method exists
* to prevent automatic set methods from setting a read-only
* property while allowing a manual set method to override this
* default behaviour.
*/
@Override
public void unlockReadOnlyPropertiesOnce() {
this.readOnlyPropertiesUnlocked = true;
}
@Override
public void removeProperty(final PropertyKey key) throws FrameworkException {
if (this.dbNode != null) {
if (key == null) {
logger.log(Level.SEVERE, "Tried to set property with null key (action was denied)");
return;
}
// check for read-only properties
if (key.isReadOnly()) {
// allow super user to set read-only properties
if (readOnlyPropertiesUnlocked || securityContext.isSuperUser()) {
// permit write operation once and
// lock read-only properties again
readOnlyPropertiesUnlocked = false;
} else {
throw new FrameworkException(this.getType(), new ReadOnlyPropertyToken(key));
}
}
dbNode.removeProperty(key.dbName());
// remove from index
removeFromIndex(key);
}
}
//~--- get methods ----------------------------------------------------
@Override
public PropertyKey getDefaultSortKey() {
return AbstractNode.name;
}
@Override
public String getDefaultSortOrder() {
return GraphObjectComparator.ASCENDING;
}
@Override
public String getType() {
return getProperty(AbstractNode.type);
}
@Override
public PropertyContainer getPropertyContainer() {
return dbNode;
}
/**
* Get name from underlying db node
*
* If name is null, return node id as fallback
*/
@Override
public String getName() {
String name = getProperty(AbstractNode.name);
if (name == null) {
name = getNodeId().toString();
}
return name;
}
/**
* Get id from underlying db
*/
@Override
public long getId() {
if (dbNode == null) {
return -1;
}
return dbNode.getId();
}
@Override
public String getUuid() {
if (cachedUuid == null) {
cachedUuid = getProperty(GraphObject.id);
}
return cachedUuid;
}
public Long getNodeId() {
return getId();
}
public String getIdString() {
return Long.toString(getId());
}
/**
* Indicates whether this node is visible to public users.
*
* @return whether this node is visible to public users
*/
public boolean getVisibleToPublicUsers() {
return getProperty(visibleToPublicUsers);
}
/**
* Indicates whether this node is visible to authenticated users.
*
* @return whether this node is visible to authenticated users
*/
public boolean getVisibleToAuthenticatedUsers() {
return getProperty(visibleToPublicUsers);
}
/**
* Indicates whether this node is hidden.
*
* @return whether this node is hidden
*/
public boolean getHidden() {
return getProperty(hidden);
}
/**
* Indicates whether this node is deleted.
*
* @return whether this node is deleted
*/
public boolean getDeleted() {
return getProperty(deleted);
}
/**
* Returns the property set for the given view as an Iterable.
*
* @param propertyView
* @return the property set for the given view
*/
@Override
public Iterable<PropertyKey> getPropertyKeys(final String propertyView) {
// check for custom view in content-type field
if (securityContext != null && securityContext.hasCustomView()) {
final Set<PropertyKey> keys = new LinkedHashSet<>(StructrApp.getConfiguration().getPropertySet(entityType, propertyView));
final Set<String> customView = securityContext.getCustomView();
for (Iterator<PropertyKey> it = keys.iterator(); it.hasNext();) {
if (!customView.contains(it.next().jsonName())) {
it.remove();
}
}
return keys;
}
// this is the default if no application/json; properties=[...] content-type header is present on the request
return StructrApp.getConfiguration().getPropertySet(entityType, propertyView);
}
/**
* Return property value which is used for indexing.
*
* This is useful f.e. to filter markup from HTML to index only text,
* or to get dates as long values.
*
* @param key
* @return property value for indexing
*/
@Override
public Object getPropertyForIndexing(final PropertyKey key) {
Object value = getProperty(key, false, null);
if (value != null) {
return value;
}
return getProperty(key);
}
/**
* Returns the (converted, validated, transformed, etc.) property for the given
* property key.
*
* @param <T>
* @param key the property key to retrieve the value for
* @return the converted, validated, transformed property value
*/
@Override
public <T> T getProperty(final PropertyKey<T> key) {
return getProperty(key, true, null);
}
@Override
public <T> T getProperty(final PropertyKey<T> key, final org.neo4j.helpers.Predicate<GraphObject> predicate) {
return getProperty(key, true, predicate);
}
private <T> T getProperty(final PropertyKey<T> key, boolean applyConverter, final org.neo4j.helpers.Predicate<GraphObject> predicate) {
// early null check, this should not happen...
if (key == null || key.dbName() == null) {
return null;
}
return key.getProperty(securityContext, this, applyConverter, predicate);
}
public String getPropertyMD5(final PropertyKey key) {
Object value = getProperty(key);
if (value instanceof String) {
return DigestUtils.md5Hex((String) value);
} else if (value instanceof byte[]) {
return DigestUtils.md5Hex((byte[]) value);
}
logger.log(Level.WARNING, "Could not create MD5 hex out of value {0}", value);
return null;
}
/**
* Returns the property value for the given key as a Comparable
*
* @param key the property key to retrieve the value for
* @return the property value for the given key as a Comparable
*/
@Override
public <T> Comparable getComparableProperty(final PropertyKey<T> key) {
if (key != null) {
final T propertyValue = getProperty(key);
// check property converter
PropertyConverter<T, ?> converter = key.databaseConverter(securityContext, this);
if (converter != null) {
try {
return converter.convertForSorting(propertyValue);
} catch (Throwable t) {
t.printStackTrace();
logger.log(Level.WARNING, "Unable to convert property {0} of type {1}: {2}", new Object[] {
key.dbName(),
getClass().getSimpleName(),
t.getMessage()
});
}
}
// conversion failed, may the property value itself is comparable
if (propertyValue instanceof Comparable) {
return (Comparable)propertyValue;
}
// last try: convertFromInput to String to make comparable
if (propertyValue != null) {
return propertyValue.toString();
}
}
return null;
}
/**
* Returns the property value for the given key as a Iterable
*
* @param propertyKey the property key to retrieve the value for
* @return the property value for the given key as a Iterable
*/
public Iterable getIterableProperty(final PropertyKey<? extends Iterable> propertyKey) {
return (Iterable)getProperty(propertyKey);
}
/**
* Returns a list of related nodes for which a modification propagation is configured
* via the relationship. Override this method to return a set of nodes that should
* receive propagated modifications.
*
* @return a set of nodes to which modifications should be propagated
*/
public Set<AbstractNode> getNodesForModificationPropagation() {
return null;
}
/**
* Returns database node.
*
* @return the database node
*/
@Override
public Node getNode() {
return dbNode;
}
private <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> Iterable<R> getIncomingRelationshipsAsSuperUser(final Class<R> type) {
final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance());
final R template = getRelationshipForType(type);
return new IterableAdapter<>(template.getSource().getRawSource(SecurityContext.getSuperUserInstance(), dbNode, null), factory);
}
/**
* Return the (cached) incoming relationship between this node and the
* given principal which holds the security information.
*
* @param p
* @return incoming security relationship
*/
@Override
public Security getSecurityRelationship(final Principal p) {
if (p == null) {
return null;
}
for (Security r : getIncomingRelationshipsAsSuperUser(Security.class)) {
if (p.equals(r.getSourceNode())) {
return r;
}
}
return null;
}
@Override
public <R extends AbstractRelationship> Iterable<R> getRelationships() {
return new IterableAdapter<>(dbNode.getRelationships(), new RelationshipFactory<R>(securityContext));
}
@Override
public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> Iterable<R> getRelationships(final Class<R> type) {
final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
final R template = getRelationshipForType(type);
final Direction direction = template.getDirectionForType(entityType);
final RelationshipType relType = template;
return new IterableAdapter<>(dbNode.getRelationships(relType, direction), factory);
}
@Override
public <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, OneStartpoint<A>, T>> R getIncomingRelationship(final Class<R> type) {
final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
final R template = getRelationshipForType(type);
final Relationship relationship = template.getSource().getRawSource(securityContext, dbNode, null);
if (relationship != null) {
return factory.adapt(relationship);
}
return null;
}
@Override
public <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, ManyStartpoint<A>, T>> Iterable<R> getIncomingRelationships(final Class<R> type) {
final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
final R template = getRelationshipForType(type);
return new IterableAdapter<>(template.getSource().getRawSource(securityContext, dbNode, null), factory);
}
@Override
public <A extends NodeInterface, B extends NodeInterface, S extends Source, R extends Relation<A, B, S, OneEndpoint<B>>> R getOutgoingRelationship(final Class<R> type) {
final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
final R template = getRelationshipForType(type);
final Relationship relationship = template.getTarget().getRawSource(securityContext, dbNode, null);
if (relationship != null) {
return factory.adapt(relationship);
}
return null;
}
@Override
public <A extends NodeInterface, B extends NodeInterface, S extends Source, R extends Relation<A, B, S, ManyEndpoint<B>>> Iterable<R> getOutgoingRelationships(final Class<R> type) {
final RelationshipFactory<R> factory = new RelationshipFactory<>(securityContext);
final R template = getRelationshipForType(type);
return new IterableAdapter<>(template.getTarget().getRawSource(securityContext, dbNode, null), factory);
}
@Override
public <R extends AbstractRelationship> Iterable<R> getIncomingRelationships() {
return new IterableAdapter<>(dbNode.getRelationships(Direction.INCOMING), new RelationshipFactory<R>(securityContext));
}
@Override
public <R extends AbstractRelationship> Iterable<R> getOutgoingRelationships() {
return new IterableAdapter<>(dbNode.getRelationships(Direction.OUTGOING), new RelationshipFactory<R>(securityContext));
}
/**
* Return statistical information on all relationships of this node
*
* @param dir
* @return number of relationships
*/
public Map<RelationshipType, Long> getRelationshipInfo(final Direction dir) throws FrameworkException {
return StructrApp.getInstance(securityContext).command(NodeRelationshipStatisticsCommand.class).execute(this, dir);
}
/**
* Returns the owner node of this node, following an INCOMING OWNS relationship.
*
* @return the owner node of this node
*/
@Override
public Principal getOwnerNode() {
if (cachedOwnerNode == null) {
final Ownership ownership = getIncomingRelationshipAsSuperUser(PrincipalOwnsNode.class);
if (ownership != null) {
Principal principal = ownership.getSourceNode();
cachedOwnerNode = (Principal) principal;
}
}
return cachedOwnerNode;
}
/**
* Returns the database ID of the owner node of this node.
*
* @return the database ID of the owner node of this node
*/
public Long getOwnerId() {
return getOwnerNode().getId();
}
/**
* Return a list with the connected principals (user, group, role)
* @return list with principals
*/
public List<Principal> getSecurityPrincipals() {
List<Principal> principalList = new LinkedList<>();
// check any security relationships
for (Security r : getIncomingRelationshipsAsSuperUser(Security.class)) {
// check security properties
Principal principalNode = r.getSourceNode();
principalList.add(principalNode);
}
return principalList;
}
private <A extends NodeInterface, B extends NodeInterface, T extends Target, R extends Relation<A, B, OneStartpoint<A>, T>> R getIncomingRelationshipAsSuperUser(final Class<R> type) {
final RelationshipFactory<R> factory = new RelationshipFactory<>(SecurityContext.getSuperUserInstance());
final R template = getRelationshipForType(type);
final Relationship relationship = template.getSource().getRawSource(SecurityContext.getSuperUserInstance(), dbNode, null);
if (relationship != null) {
return factory.adapt(relationship);
}
return null;
}
/**
* Return true if this node has a relationship of given type and direction.
*
* @param <A>
* @param <B>
* @param <S>
* @param <T>
* @param type
* @return relationships
*/
public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target> boolean hasRelationship(final Class<? extends Relation<A, B, S, T>> type) {
return this.getRelationships(type).iterator().hasNext();
}
public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> boolean hasIncomingRelationships(final Class<R> type) {
return getRelationshipForType(type).getSource().hasElements(securityContext, dbNode, null);
}
public <A extends NodeInterface, B extends NodeInterface, S extends Source, T extends Target, R extends Relation<A, B, S, T>> boolean hasOutgoingRelationships(final Class<R> type) {
return getRelationshipForType(type).getTarget().hasElements(securityContext, dbNode, null);
}
// ----- interface AccessControllable -----
@Override
public boolean isGranted(final Permission permission, final Principal principal) {
if (principal == null) {
return false;
}
// just in case ...
if (permission == null) {
return false;
}
// superuser
if (principal instanceof SuperUser) {
return true;
}
// user has full control over his/her own user node
if (this.equals(principal)) {
return true;
}
Security r = getSecurityRelationship(principal);
if ((r != null) && r.isAllowed(permission)) {
return true;
}
// Now check possible parent principals
for (Principal parent : principal.getParents()) {
if (isGranted(permission, parent)) {
return true;
}
}
return false;
}
@Override
public boolean onCreation(SecurityContext securityContext, ErrorBuffer errorBuffer) throws FrameworkException {
return isValid(errorBuffer);
}
@Override
public boolean onModification(SecurityContext securityContext, ErrorBuffer errorBuffer) throws FrameworkException {
return isValid(errorBuffer);
}
@Override
public boolean onDeletion(SecurityContext securityContext, ErrorBuffer errorBuffer, PropertyMap properties) throws FrameworkException {
return true;
}
@Override
public void afterCreation(SecurityContext securityContext) {
}
@Override
public void afterModification(SecurityContext securityContext) {
}
@Override
public void afterDeletion(SecurityContext securityContext, PropertyMap properties) {
}
@Override
public void ownerModified(SecurityContext securityContext) {
}
@Override
public void securityModified(SecurityContext securityContext) {
}
@Override
public void locationModified(SecurityContext securityContext) {
}
@Override
public void propagatedModification(SecurityContext securityContext) {
}
@Override
public boolean isValid(ErrorBuffer errorBuffer) {
boolean error = false;
error |= ValidationHelper.checkStringNotBlank(this, id, errorBuffer);
error |= ValidationHelper.checkStringNotBlank(this, type, errorBuffer);
return !error;
}
@Override
public boolean isVisibleToPublicUsers() {
return getVisibleToPublicUsers();
}
@Override
public boolean isVisibleToAuthenticatedUsers() {
return getProperty(visibleToAuthenticatedUsers);
}
@Override
public boolean isNotHidden() {
return !getHidden();
}
@Override
public boolean isHidden() {
return getHidden();
}
@Override
public Date getVisibilityStartDate() {
return getProperty(visibilityStartDate);
}
@Override
public Date getVisibilityEndDate() {
return getProperty(visibilityEndDate);
}
@Override
public Date getCreatedDate() {
return getProperty(createdDate);
}
@Override
public Date getLastModifiedDate() {
return getProperty(lastModifiedDate);
}
// ----- end interface AccessControllable -----
public boolean isNotDeleted() {
return !getDeleted();
}
@Override
public boolean isDeleted() {
return getDeleted();
}
/**
* Return true if node is the root node
*
* @return isRootNode
*/
public boolean isRootNode() {
return getId() == 0;
}
public boolean isVisible() {
return securityContext.isVisible(this);
}
/**
* Set a property in database backend. This method needs to be wrappend into
* a StructrTransaction, otherwise Neo4j will throw a NotInTransactionException!
* Set property only if value has changed.
*
* @param <T>
* @param key
* @throws org.structr.common.error.FrameworkException
*/
@Override
public <T> void setProperty(final PropertyKey<T> key, final T value) throws FrameworkException {
T oldValue = getProperty(key);
// check null cases
if ((oldValue == null) && (value == null)) {
return;
}
// no old value exists, set property
if ((oldValue == null) && (value != null)) {
setPropertyInternal(key, value);
return;
}
// old value exists and is NOT equal
if ((oldValue != null) && !oldValue.equals(value)) {
setPropertyInternal(key, value);
}
}
private <T> void setPropertyInternal(final PropertyKey<T> key, final T value) throws FrameworkException {
if (key == null) {
logger.log(Level.SEVERE, "Tried to set property with null key (action was denied)");
throw new FrameworkException(getClass().getSimpleName(), new NullArgumentToken(base));
}
// check for read-only properties
if (key.isReadOnly() || (key.isWriteOnce() && (dbNode != null) && dbNode.hasProperty(key.dbName()))) {
if (readOnlyPropertiesUnlocked || securityContext.isSuperUser()) {
// permit write operation once and
// lock read-only properties again
readOnlyPropertiesUnlocked = false;
} else {
throw new FrameworkException(getClass().getSimpleName(), new ReadOnlyPropertyToken(key));
}
}
key.setProperty(securityContext, this, value);
}
@Override
public void addToIndex() {
for (PropertyKey key : StructrApp.getConfiguration().getPropertySet(entityType, PropertyView.All)) {
if (key.isIndexed()) {
key.index(this, this.getPropertyForIndexing(key));
}
}
}
@Override
public void updateInIndex() {
removeFromIndex();
addToIndex();
}
@Override
public void removeFromIndex() {
for (Index<Node> index : Services.getInstance().getService(NodeService.class).getNodeIndices()) {
synchronized (index) {
index.remove(dbNode);
}
}
}
public void removeFromIndex(PropertyKey key) {
for (Index<Node> index : Services.getInstance().getService(NodeService.class).getNodeIndices()) {
synchronized (index) {
index.remove(dbNode, key.dbName());
}
}
}
@Override
public void indexPassiveProperties() {
for (PropertyKey key : StructrApp.getConfiguration().getPropertySet(entityType, PropertyView.All)) {
if (key.isPassivelyIndexed()) {
key.index(this, this.getPropertyForIndexing(key));
}
}
}
public static <A extends NodeInterface, B extends NodeInterface, R extends Relation<A, B, ?, ?>> R getRelationshipForType(final Class<R> type) {
R instance = (R)relationshipTemplateInstanceCache.get(type);
if (instance == null) {
try {
instance = type.newInstance();
relationshipTemplateInstanceCache.put(type, instance);
} catch (Throwable t) {
// TODO: throw meaningful exception here,
// should be a RuntimeException that indicates
// wrong use of Relationships etc.
t.printStackTrace();
}
}
return instance;
}
@Override
public String getPropertyWithVariableReplacement(SecurityContext securityContext, ActionContext renderContext, PropertyKey<String> key) throws FrameworkException {
return SchemaHelper.getPropertyWithVariableReplacement(securityContext, this, renderContext, key);
}
@Override
public String replaceVariables(final SecurityContext securityContext, final ActionContext actionContext, final Object rawValue) throws FrameworkException {
return SchemaHelper.replaceVariables(securityContext, this, actionContext, rawValue);
}
protected String[] split(final String source) {
ArrayList<String> tokens = new ArrayList<>(20);
boolean inDoubleQuotes = false;
boolean inSingleQuotes = false;
boolean ignoreNext = false;
int len = source.length();
int level = 0;
StringBuilder currentToken = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = source.charAt(i);
// do not strip away separators in nested functions!
if ((level != 0) || (c != ',')) {
currentToken.append(c);
}
if (ignoreNext) {
ignoreNext = false;
continue;
}
switch (c) {
case '\\':
ignoreNext = true;
break;
case '(':
level++;
break;
case ')':
level--;
break;
case '"':
if (inDoubleQuotes) {
inDoubleQuotes = false;
level--;
} else {
inDoubleQuotes = true;
level++;
}
break;
case '\'':
if (inSingleQuotes) {
inSingleQuotes = false;
level--;
} else {
inSingleQuotes = true;
level++;
}
break;
case ',':
if (level == 0) {
tokens.add(currentToken.toString().trim());
currentToken.setLength(0);
}
break;
}
}
if (currentToken.length() > 0) {
tokens.add(currentToken.toString().trim());
}
return tokens.toArray(new String[0]);
}
}