/*
* Copyright (C) 2014, C. Ramakrishnan / Auracle.
* All rights reserved.
*
* This code is licensed under the BSD 3-Clause license.
* See file LICENSE (or LICENSE.html) for more information.
*/
package com.illposed.osc.utility;
import com.illposed.osc.AddressSelector;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Checks whether an OSC message address matches a given wildcard expression,
* as specified in the OSC protocol specification.
* For details, see the "OSC Message Dispatching and Pattern Matching" section on
* <a href="http://opensoundcontrol.org/spec-1_0">the OSC 1.0 specification</a>.
* Also supports the path-traversal wildcard "//", as specified in OSC 1.1 (borrowed from XPath).
*
* <p>
* A coarse history of the code in the function
* {@link OSCPatternAddressSelector#matches(String, String)},
* from the origin to JavaOSC:
* </p>
* <ol>
* <li>
* <b>robust glob pattern matcher</b><br>
* language: <i>C</i><br>
* author: <i>ozan s. yigit/dec 1994</i><br>
* matching code license: <i>public domain</i><br>
* <a href="http://www.cse.yorku.ca/~oz/glob.bun">source location</a><br>
* </li>
* <li>
* <b>Open SoundControl kit</b><br>
* language: <i>C++</i><br>
* matching code license: <i>public domain</i><br>
* library license: <i>LGPL 2.1+</i><br>
* <a href="http://archive.cnmat.berkeley.edu/OpenSoundControl/src/OSC-Kit/OSC-pattern-match.c">source location</a><br>
* </li>
* <li>
* <b>LibLO</b><br>
* language: <i>C++</i><br>
* library license: <i>LGPL 2.1+</i><br>
* <a href="https://sourceforge.net/p/liblo/git/ci/master/tree/src/pattern_match.c">source location</a><br>
* </li>
* <li>
* <b>JavaOSC</b><br>
* language: <i>Java</i><br>
* matching code license: <i>public domain</i><br>
* library license: <i>BSD 3-Clause</i><br>
* </li>
* </ol>
*/
public class OSCPatternAddressSelector implements AddressSelector {
private final List<String> patternParts;
public OSCPatternAddressSelector(String selector) {
this.patternParts = splitIntoParts(selector);
}
@Override
public boolean matches(String messageAddress) {
List<String> messageAddressParts = splitIntoParts(messageAddress);
return matches(patternParts, 0, messageAddressParts, 0);
}
/**
* Splits an OSC message address or address selector pattern into parts that are convenient
* during the matching process.
* @param addressOrPattern to be split into parts, e.g.: "/hello/", "/hello//world//"
* @return the given address or pattern split into parts: {"hello"}, {"hello, "", "world", ""}
*/
private static List<String> splitIntoParts(String addressOrPattern) {
List<String> parts = new ArrayList<String>(Arrays.asList(addressOrPattern.split("/", -1)));
if (addressOrPattern.startsWith("/")) {
// as "/hello" gets split into {"", "hello"}, we remove the first empty entry,
// so we end up with {"hello"}
parts.remove(0);
}
if (addressOrPattern.endsWith("/")) {
// as "hello/" gets split into {"hello", ""}, we also remove the last empty entry,
// so we end up with {"hello"}
parts.remove(parts.size() - 1);
}
return Collections.unmodifiableList(parts);
}
/**
* Tries to match an OSC <i>Address Pattern</i> to a selector,
* both already divided into their parts.
* @param patternParts all the parts of the pattern
* @param ppi index/pointer to the current part of the pattern we are looking at
* @param messageAddressParts all the parts of the address
* @param api index/pointer to the current part of the address we are looking at
* @return true if the address matches, false otherwise
*/
private static boolean matches(List<String> patternParts, int ppi, List<String> messageAddressParts, int api) {
while (ppi < patternParts.size()) {
// There might be some path-traversal wildcards (PTW) "//" in the pattern.
// "//" in the pattern translates to an empty String ("") in the pattern parts.
// We skip all consecutive "//"s at the current pattern position.
boolean pathTraverser = false;
while ((ppi < patternParts.size()) && patternParts.get(ppi).isEmpty()) {
ppi++;
pathTraverser = true;
}
// ppi is now at the end, or at the first non-PTW part
if (pathTraverser) {
if (ppi == patternParts.size()) {
// trailing PTW matches the whole rest of the address
return true;
}
while (api < messageAddressParts.size()) {
if (matches(messageAddressParts.get(api), patternParts.get(ppi))
&& matches(patternParts, ppi + 1, messageAddressParts, api + 1))
{
return true;
}
api++;
}
// end of address parts reached, but there are still non-PTW pattern parts
// left
return false;
} else {
if ((ppi == patternParts.size()) != (api == messageAddressParts.size())) {
// end of pattern, no trailing PTW, but there are still address parts left
// OR
// end of address, but there are still non-PTW pattern parts left
return false;
}
if (!matches(messageAddressParts.get(api), patternParts.get(ppi))) {
return false;
}
api++;
}
ppi++;
}
return (api == messageAddressParts.size());
}
/**
* Tries to match an OSC <i>Address Pattern</i> part to a part of
* a selector.
* This code was copied and adapted from LibLo,
* and is licensed under the Public Domain.
* For more details see: {@link OSCPatternAddressSelector}.
* @param str address part
* @param p pattern part
* @return true if the address part matches, false otherwise
*/
private static boolean matches(String str, String p) {
boolean negate;
boolean match;
char c;
int si = 0;
int pi = 0;
while (pi < p.length()) {
if ((si == str.length()) && p.charAt(pi) != '*') {
return false;
}
c = p.charAt(pi++);
switch (c) {
case '*':
while ((pi < p.length()) && p.charAt(pi) == '*' && p.charAt(pi) != '/') {
pi++;
}
if (pi == p.length()) {
return true;
}
// if (p.charAt(pi) != '?' && p.charAt(pi) != '[' && p.charAt(pi) != '\\')
if (p.charAt(pi) != '?' && p.charAt(pi) != '[' && p.charAt(pi) != '{') {
while (si < str.length() && p.charAt(pi) != str.charAt(si)) {
si++;
}
}
while (si < str.length()) {
if (matches(str.substring(si), p.substring(pi))) {
return true;
}
si++;
}
return false;
case '?':
if (si < str.length()) {
break;
}
return false;
/*
* set specification is inclusive, that is [a-z] is a, z and
* everything in between. this means [z-a] may be interpreted
* as a set that contains z, a and nothing in between.
*/
case '[':
if (p.charAt(pi) == '!') {
negate = true;
pi++;
} else {
negate = false;
}
match = false;
while (!match && (pi < p.length())) {
c = p.charAt(pi++);
if (pi == p.length()) {
return false;
}
if (p.charAt(pi) == '-') { // c-c
pi++;
if (pi == p.length()) {
return false;
}
if (p.charAt(pi) != ']') {
if (str.charAt(si) == c || str.charAt(si) == p.charAt(pi)
|| (str.charAt(si) > c && str.charAt(si) < p.charAt(pi)))
{
match = true;
}
} else { // c-]
if (str.charAt(si) >= c) {
match = true;
}
break;
}
} else { // cc or c]
if (c == str.charAt(si)) {
match = true;
}
if (p.charAt(pi) != ']') {
if (p.charAt(pi) == str.charAt(si)) {
match = true;
}
} else {
break;
}
}
}
if (negate == match) {
return false;
}
// if there is a match, skip past the cset and continue on
while (pi < p.length() && p.charAt(pi) != ']') {
pi++;
}
if (pi++ == p.length()) { // oops!
return false;
}
break;
// {astring,bstring,cstring}
case '{':
// p.charAt(pi) is now first character in the {brace list}
int place = si; // to backtrack
int remainder = pi; // to forwardtrack
// find the end of the brace list
while ((remainder < p.length()) && (p.charAt(remainder) != '}')) {
remainder++;
}
if (remainder == p.length()) /* oops! */ {
return false;
}
remainder++;
c = p.charAt(pi++);
while (pi <= p.length()) {
if (c == ',') {
if (matches(str.substring(si), p.substring(remainder))) {
return true;
} else {
// backtrack on test string
si = place;
// continue testing,
// skip comma
if (pi++ == p.length()) { // oops
return false;
}
}
} else if (c == '}') {
// continue normal pattern matching
if ((pi == p.length()) && (si == str.length())) {
return true;
}
si--; // str is incremented again below
break;
} else if (c == str.charAt(si)) {
si++;
if ((si == str.length()) && (remainder < p.length())) {
return false;
}
} else { // skip to next comma
si = place;
while ((pi < p.length()) && (p.charAt(pi) != ',') && (p.charAt(pi) != '}')) {
pi++;
}
if (pi < p.length()) {
if (p.charAt(pi) == ',') {
pi++;
} else if (p.charAt(pi) == '}') {
return false;
}
}
}
c = p.charAt(pi++);
}
break;
/*
* Not part of OSC pattern matching
case '\\':
if (p.charAt(pi)) {
c = p.charAt(pi)++;
}
*/
default:
if (c != str.charAt(si)) {
return false;
}
break;
}
si++;
}
return (si == str.length());
}
}