blob: a91ccfe3a3bb492156f13f058995d4edb26feb11 [file] [log] [blame]
/*
* Copyright (c) 2014, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Tomas Kraus - Initial API and implementation
package org.eclipse.persistence.internal.core.queries;
import java.util.ArrayDeque;
import org.eclipse.persistence.internal.helper.StringHelper;
import static org.eclipse.persistence.internal.helper.StringHelper.TAB;
import static org.eclipse.persistence.internal.helper.StringHelper.LF;
import static org.eclipse.persistence.internal.helper.StringHelper.FF;
import static org.eclipse.persistence.internal.helper.StringHelper.CR;
import static org.eclipse.persistence.internal.helper.StringHelper.SPACE;
// This class have huge performance impact because convert method is used very often.
/**
* INTERNAL:
* AttributeGroup attribute names converter.
*/
public class CoreAttributeConverter {
/**
* String containing '.'.
*/
private static final String DOT = Character.toString(StringHelper.DOT);
// Path convert state machine
// Splits String around '.' character. Path elements starting or ending
// with whitespace or of zero length are considered as illegal.
// ,---+
// | x V
// x +----+ x
// ,---------->| CH |<-----------,
// | +----+ |
// +-----+ '.' | | ' ' +----+
// | DOT |<--------' `--------->| SP |
// +-----+ +----+
// | ' ', '.' +-----+ '.' |
// `---------->| ERR |<----------'
// +-----+
/**
* Path convert state machine internal states.
*/
private enum ConvertState {
/** Initial state or after path elements separator received. */
DOT,
/** After space in the middle of path (at least one regular character received in path element). */
SP,
/** After regular path character received in path element. */
CH,
}
// PERF: Whitespace characters set reduced to TAB, LF, FF, CR and SPACE.
// Everything is one long and ugly method.
/**
* INTERNAL:
* Splits given <code>nameOrPath[0]</code> argument around <code>'.'</code> character
* when <code>nameOrPath.length</code> is equal to 1. Arrays of <code>nameOrPath</code>
* containing more than one element are only validated an passed without any changes.
* Zero length arrays are considered as invalid.
*
* @param nameOrPath {@link String} to be split.
* @return An array of {@link String}s computed by splitting provided
* <code>nameOrPath[0]</code> argument around <code>'.'</code> character.
* @throws IllegalArgumentException If <code>nameOrPath</code> argument is <code>null</code>
* or any element to be returned after split is <code>null</code>, empty or contains
* whitespace at the beginning or end.
*/
@SuppressWarnings({"fallthrough"})
public static final String[] convert(final String... nameOrPath)
throws IllegalArgumentException {
if (nameOrPath == null) {
throw new IllegalArgumentException("Name or path value is null");
}
switch(nameOrPath.length) {
// Empty String array.
case 0:
throw new IllegalArgumentException("Name or path value size is zero");
// Single String: may contain path elements separated by '.'.
case 1:
final String str = nameOrPath[0];
if (str == null) {
throw new IllegalArgumentException("Name or path value is null");
}
final int len = str.length();
if (!str.contains(DOT)) {
switch(len) {
case 0:
throw new IllegalArgumentException("Empty name or path value");
case 1:
switch(str.charAt(0)) {
case TAB: case LF: case FF: case CR: case SPACE:
throw new IllegalArgumentException("Name or path value starts with whitespace.");
}
default:
switch(str.charAt(0)) {
case TAB: case LF: case FF: case CR: case SPACE:
throw new IllegalArgumentException("Name or path value starts with whitespace.");
}
switch(str.charAt(len - 1)) {
case TAB: case LF: case FF: case CR: case SPACE:
throw new IllegalArgumentException("Name or path value ends with whitespace.");
}
}
return nameOrPath;
}
final char[] chars = nameOrPath[0].toCharArray();
// Current character being processed.
char c;
// Current state machine state.
ConvertState s = ConvertState.DOT;
// Index of current character in parsed String
int index;
// Index of 1st path element regular character.
int begIndex = 0;
// Path elements storage (not needed when there is just one element).
ArrayDeque<String> elements = null;
for (index = 0; index < len; index++) {
c = chars[index];
switch (s) {
// Initial state or after path elements separator received.
case DOT:
switch (c) {
// Path elements separator at the beginning or two path element separators
// next to each other results in zero length path.
case StringHelper.DOT:
throw new IllegalArgumentException("Name or path value contains empty path element");
// Whitespace at the beginning.
case TAB: case LF: case FF: case CR: case SPACE:
throw new IllegalArgumentException("Path element starts with whitespace.");
// First regular character.
default:
s = ConvertState.CH;
}
break;
// After space in the middle of path.
case SP:
switch (c) {
// Path elements separator after whitespace.
case StringHelper.DOT:
throw new IllegalArgumentException("Path element ends with whitespace.");
// Whitespace after whitespace.
case TAB: case LF: case FF: case CR: case SPACE:
break;
// Regular character after whitespace in the middle of path.
default:
s = ConvertState.CH;
}
break;
// After regular path character received.
case CH:
switch (c) {
// Path element separator after regular path character.
case StringHelper.DOT:
// Lazy initialization of elements storage.
if (elements == null) {
elements = new ArrayDeque(4);
}
// Store finished path element.
elements.addLast(new String(chars, begIndex, index - begIndex));
// Next character starts next path element.
begIndex = index + 1;
s = ConvertState.DOT;
break;
// Space in the middle of path element.
case TAB: case LF: case FF: case CR: case SPACE:
s = ConvertState.SP;
}
}
}
// Now process end of input attribute.
switch (s) {
// Last character is path elements separator so there is an empty
// path element at the end of string or input string was empty.
case DOT:
throw new IllegalArgumentException("Empty name or path value or last path element");
// Last character is whitespace.
case SP:
throw new IllegalArgumentException("Name or path value ends with space");
// Remaining CH as default: Last character is regular character.
default:
// There was no separator in the name or path value. Original
// value is returned.
if (elements == null) {
return nameOrPath;
// Return array of path elements found. Last element is still
// in input String.
} else {
int n = 0, count = elements.size();
String[] paths = new String[count + 1];
while ((paths[n++] = elements.pollFirst()) != null);
paths[count] = new String(chars, begIndex, index - begIndex);
return paths;
}
}
// Multiple Strings: Validate and pass them as they are.
default:
String item;
for (int i = 0; i < nameOrPath.length; i++) {
item = nameOrPath[i];
if (item == null) {
throw new IllegalArgumentException("Name or path value at index "
+ i + " is null");
}
final int itemLen = item.length();
// Empty String.
if (itemLen == 0) {
throw new IllegalArgumentException("Name or path value at index "
+ i + " is empty String");
// String contains at least one character.
} else {
switch(item.charAt(0)) {
case TAB: case LF: case FF: case CR: case SPACE:
throw new IllegalArgumentException("Name or path value at index "
+ i + " starts with whitespace");
}
// String contains more than one character.
if (itemLen > 0) {
switch(item.charAt(itemLen - 1)) {
case TAB: case LF: case FF: case CR: case SPACE:
throw new IllegalArgumentException("Name or path value at index "
+ i + " ends with whitespace");
}
}
}
}
return nameOrPath;
}
}
/**
* INTERNAL:
* This class is just an envelope for static methods so no instances are allowed.
*/
private CoreAttributeConverter() {
throw new UnsupportedOperationException("Instances of CoreAttributeConverter are not allowed");
}
}