/*
* 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.addthis.hydra.data.query.op;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import com.addthis.basis.util.LessStrings;
import com.addthis.bundle.core.Bundle;
import com.addthis.bundle.util.ValueUtil;
import com.addthis.bundle.value.ValueFactory;
import com.addthis.bundle.value.ValueObject;
import com.addthis.hydra.data.query.AbstractRowOp;
import io.netty.channel.ChannelProgressivePromise;
/**
* <p>This query operation <span class="hydra-summary">performs postfix (RPN)
* calculator string operations</span>.
* <p/>
* <p>This filter uses a calculation stack to perform string operations. The stack is initially empty
* and values can be pushed or popped from the top of the stack. String operations can be performed
* on the element(s) at the top of the stack.
* <pre style="font-family:courier;font-size:15px">
* num=OP,OP,OP,...
* <p/>
* OP := LOAD_OP | CALC_OP
* LOAD_OP := LOAD_COL | LOAD_VAL
* LOAD_COL := ("c" | "col" ) INT
* LOAD_VAL := ("v" | "val" ) STRING
* CALC_OP := [see table for operations]
* INT := DIGIT+
* DIGIT := ["0" - "9"]</pre>
* <p/>
* <p>The following operations below are available. Each operation proceeds in three steps:
* (1) Zero or more values are popped off the stack, (2) an operation is performed on these values,
* and (3) zero or more values are pushed onto the stack. For convenience the following table
* labels the first element popped off the stack as "a" (the topmost element), the second
* element as "b", etc. The variables "X","Y","Z", etc. represent numbers in the command sequence.
* <p/>
* <table width="80%" class="num">
* <tr>
* <th width="15%">name</th>
* <th width="10%">pop count</th>
* <th width="65%">operation</th>
* <th width="10%">push count</th>
* </tr>
* <tr>
* <td>"cat" or "+"</td>
* <td>2</td>
* <td>concatenate two strings</td>
* <td>1</td>
* </tr>
* <tr>
* <td>"range" or ":"</td>
* <td>3</td>
* <td>take the range of string c from offset b to offset a</td>
* <td>1</td>
* </tr>
* <tr>
* <td>"split" or "/"</td>
* <td>3</td>
* <td>split string c using separator b and push element number a from the result</td>
* <td>1</td>
* </tr>
* <tr>
* <td>"set" or "="</td>
* <td>2</td>
* <td>set column a to string b</td>
* <td>0</td>
* </tr>
* <tr>
* <td>"eq" or "=="</td>
* <td>2</td>
* <td>if string equality is false then end the calculation</td>
* <td>0</td>
* </tr>
* <tr>
* <td>"neq" or "!="</td>
* <td>2</td>
* <td>if string equality is true then end the calculation</td>
* <td>0</td>
* </tr>
* <tr>
* <td>"cX"</td>
* <td>0</td>
* <td>push the string in column X onto the stack</td>
* <td>len(args)</td>
* </tr>
* <tr>
* <td>"vX"</td>
* <td>0</td>
* <td>push the string X onto the stack</td>
* <td>1</td>
* </tr>
* </table>
* <p/>
* <p>Example 1:</p>
* <pre>
* A 1 art
* B 2 bot
* C 3 cog
* D 4 din
*
* str=c0,c1,cat,v2,set
*
* A 1 A1
* B 2 B2
* C 3 C3
* D 4 D4
* </pre>
* <p/>
* <p>Example 2:</p>
* <pre>
* A 1|||a art
* B 2|||b bot
* C 3|||c cog
* D 4|||d din
*
* str=c1,v|||,v0,split,v2,set,
*
* A 1 1
* B 2 2
* C 3 3
* D 4 4
* </pre>
*
* @user-reference
* @hydra-name str
*/
public class OpString extends AbstractRowOp {
private static final int OP_CAT = -1;
private static final int OP_RANGE = -2;
private static final int OP_SPLIT = -3;
private static final int OP_COLVAL = -4;
private static final int OP_CONST = -5;
private static final int OP_SET = -6;
private static final int OP_EQUAL = -7;
private static final int OP_NOTEQUAL = -8;
private final List<StringOp> ops;
public OpString(String args, ChannelProgressivePromise queryPromise) {
super(queryPromise);
String[] op = LessStrings.splitArray(args, ",");
ops = new ArrayList<>(op.length);
for (String o : op) {
if (o.equals("+") || o.equals("cat")) {
ops.add(new StringOp(OP_CAT, null));
} else if (o.equals(":") || o.equals("range")) {
ops.add(new StringOp(OP_RANGE, null));
} else if (o.equals("/") || o.equals("split")) {
ops.add(new StringOp(OP_SPLIT, null));
} else if (o.equals("=") || o.equals("set")) {
ops.add(new StringOp(OP_SET, null));
} else if (o.equals("==") || o.equals("eq")) {
ops.add(new StringOp(OP_EQUAL, null));
} else if (o.equals("!=") || o.equals("neq")) {
ops.add(new StringOp(OP_NOTEQUAL, null));
} else {
if (o.startsWith("c")) {
ops.add(new StringOp(OP_COLVAL, ValueFactory.create(o.substring(1))));
} else if (o.startsWith("v")) {
String literal = o.equals("v") ? "," : o.substring(1);
ops.add(new StringOp(OP_CONST, ValueFactory.create(literal)));
}
}
}
}
@Override
public Bundle rowOp(Bundle row) {
LinkedList<ValueObject> stack = new LinkedList<>();
long maxcol = row.getCount() - 1;
for (StringOp op : ops) {
switch (op.type) {
case OP_CAT:
ValueObject rval = stack.pop();
stack.push(ValueFactory.create(stack.pop().toString().concat(rval.toString())));
break;
case OP_RANGE:
int last = (int) stack.pop().asLong().getLong();
int first = (int) stack.pop().asLong().getLong();
String str = stack.pop().toString();
int len = str.length();
if (len == 0) {
stack.push(ValueFactory.create(""));
break;
}
if (first < 0) {
first = len + first;
}
if (last < 0) {
last = len + last;
}
if (first > len) {
first = len - 1;
}
if (last > len) {
last = len;
}
stack.push(ValueFactory.create(str.substring(first, last)));
break;
case OP_SPLIT:
int pos = (int) stack.pop().asLong().getLong();
String sep = stack.pop().toString();
str = stack.pop().toString();
String[] seg = LessStrings.splitArray(str, sep);
stack.push(ValueFactory.create(seg[pos]));
break;
case OP_COLVAL:
stack.push(getSourceColumnBinder(row).getColumn(row, (int) op.val.asLong().getLong()));
break;
case OP_CONST:
stack.push(op.val);
break;
case OP_SET:
int col = (int) stack.pop().asLong().getLong();
ValueObject val = stack.pop();
if (col < 0 || col > maxcol) {
getSourceColumnBinder(row).appendColumn(row, val);
} else {
getSourceColumnBinder(row).setColumn(row, col, val);
}
break;
case OP_EQUAL:
if (!ValueUtil.isEqual(stack.pop(), stack.pop())) {
return null;
}
break;
case OP_NOTEQUAL:
if (ValueUtil.isEqual(stack.pop(), stack.pop())) {
return null;
}
break;
default:
break;
}
}
return row;
}
/** */
private static class StringOp {
private int type;
private ValueObject val;
StringOp(int type, ValueObject val) {
this.type = type;
this.val = val;
}
}
}