advfilter

This commit is contained in:
FangfangZhao 2018-11-08 21:43:43 +08:00
parent 8598ed0175
commit 3df5bd6d6a
5 changed files with 249 additions and 133 deletions

View file

@ -20,22 +20,23 @@ package com.rebuild.server.query;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.entityhub.DisplayType;
import com.rebuild.server.entityhub.EasyMeta;
import com.rebuild.server.metadata.MetadataHelper;
import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field;
/**
* 高级查询解析器
@ -45,6 +46,8 @@ import cn.devezhao.persist4j.Field;
*/
public class AdvFilterParser {
private static final Log LOG = LogFactory.getLog(AdvFilterParser.class);
private Entity rootEntity;
private JSONObject filterExp;
@ -72,27 +75,47 @@ public class AdvFilterParser {
JSONObject values = filterExp.getJSONObject("values");
String equation = StringUtils.defaultIfBlank(filterExp.getString("equation"), "OR");
List<String> itemsSql = new ArrayList<>();
Map<Integer, String> indexItemSqls = new LinkedHashMap<>();
int noIndex = 1;
for (Object item : items) {
String itemSql = parseItem((JSONObject) item, values);
JSONObject jo = (JSONObject) item;
Integer index = jo.getInteger("index");
if (index == null) {
index = noIndex++;
}
String itemSql = parseItem(jo, values);
if (itemSql != null) {
itemsSql.add(itemSql);
indexItemSqls.put(index, itemSql);
}
}
if (itemsSql.isEmpty()) {
if (indexItemSqls.isEmpty()) {
return null;
}
if ("OR".equalsIgnoreCase(equation)) {
return "( " + StringUtils.join(itemsSql, " or ") + " )";
return "( " + StringUtils.join(indexItemSqls.values(), " or ") + " )";
} else if ("AND".equalsIgnoreCase(equation)) {
return "( " + StringUtils.join(itemsSql, " and ") + " )";
return "( " + StringUtils.join(indexItemSqls.values(), " and ") + " )";
} else {
// 高级表达式 eg. (1 AND 2) or (3 AND 4)
String tokens[] = equation.toLowerCase().split(" ");
List<String> itemSqls = new ArrayList<>();
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i];
if (StringUtils.isBlank(token)) {
continue;
}
if (NumberUtils.isDigits(token)) {
String itemSql = StringUtils.defaultIfBlank(indexItemSqls.get(Integer.valueOf(token)), "(9=9)");
itemSqls.add(itemSql);
} else if (token.equals("(") || token.equals(")") || token.equals("or") || token.equals("and")) {
itemSqls.add(token);
} else {
LOG.warn("Ignore equation token : " + token);
}
}
return StringUtils.join(itemSqls, " ");
}
// TODO 高级表达式 eg. (1 AND 2) or (3 AND 4)
return null;
}
/**
@ -106,68 +129,118 @@ public class AdvFilterParser {
return null;
}
Field metaField = rootEntity.getField(field);
if (EasyMeta.getDisplayType(metaField) == DisplayType.PICKLIST) {
field = '&' + field;
}
// Field metaField = rootEntity.getField(field);
// if (EasyMeta.getDisplayType(metaField) == DisplayType.PICKLIST) {
// field = '&' + field;
// }
String op = convOp(item.getString("op"));
String value = item.getString("value");
// 占位
if (value.matches("\\{\\d+\\}")) {
if (values == null) {
return null;
}
String valIndex = value.replaceAll("[\\{\\}]", "");
Object valReady = values.get(valIndex);
if (valReady == null) {
return null;
}
// in
if (valReady instanceof JSONArray) {
Set<String> valArray = new HashSet<>();
for (Object o : (JSONArray) valReady) {
valArray.add(quote(o.toString()));
}
if (valArray.isEmpty()) {
return null;
} else {
value = "( " + StringUtils.join(valArray, ", ") + " )";
}
} else {
value = valReady.toString();
if (StringUtils.isBlank(value)) {
return null;
}
}
}
if (op.contains("like")) {
value = '%' + value + '%';
}
StringBuffer sb = new StringBuffer(field)
.append(' ')
.append(op)
.append(' ');
// is null / is not null
if (op.contains("null")) {
return sb.toString();
}
if ("in".equals(op)) {
String value = item.getString("value");
if (StringUtils.isBlank(value)) {
LOG.warn("Invalid item of advfilter : " + item.toJSONString());
return null;
}
// 占位 {1}
if (value.matches("\\{\\d+\\}")) {
if (values == null) {
LOG.warn("Invalid item of advfilter : " + item.toJSONString());
return null;
}
String valHold = value.replaceAll("[\\{\\}]", "");
value = parseVal(values.get(valHold), op);
} else {
value = parseVal(value, op);
}
if (value == null) {
LOG.warn("Invalid item of advfilter : " + item.toJSONString());
return null;
}
// 区间
boolean isBetween = op.equals("between");
String value2 = isBetween ? parseVal(item.getString("value2"), op) : null;
if (isBetween && value2 == null) {
value2 = value;
}
// like / not like
if (op.contains("like")) {
value = '%' + value + '%';
}
if (op.equals("in") || op.equals("not in")) {
sb.append(value);
} else if (NumberUtils.isDigits(value)) {
sb.append(value);
} else if (StringUtils.isNotBlank(value)) {
sb.append(quote(value));
} else {
sb.append(quoteVal(value));
}
if (isBetween) {
// sb.insert(0, "(").append(" and ").append(quoteVal(value2)).append(")");
sb.append(" and ").append(quoteVal(value2));
}
return sb.toString();
}
private String quote(String v) {
return String.format("'%s'", StringEscapeUtils.escapeSql(v));
/**
* @param val
* @param op
* @return
*/
private String parseVal(Object val, String op) {
String value = null;
// IN
if (val instanceof JSONArray) {
Set<String> array = new HashSet<>();
for (Object o : (JSONArray) val) {
array.add(quoteVal(o.toString()));
}
if (array.isEmpty()) {
return null;
} else {
value = "( " + StringUtils.join(array, ",") + " )";
}
} else {
value = val.toString();
if (StringUtils.isBlank(value)) {
return null;
}
// 兼容 | 号分割
if (op.equals("in") || op.equals("not in")) {
Set<String> array = new HashSet<>();
for (String v : value.split("\\|")) {
array.add(quoteVal(v));
}
value = "( " + StringUtils.join(array, ",") + " )";
}
}
return value;
}
/**
* @param v
* @return
*/
private String quoteVal(String v) {
if (NumberUtils.isDigits(v)) {
return v;
} else if (StringUtils.isNotBlank(v)) {
return String.format("'%s'", StringEscapeUtils.escapeSql(v));
}
return "''";
}
/**

View file

@ -26,9 +26,14 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.server.query.AdvFilterParser;
import com.rebuild.web.BaseControll;
import com.rebuild.web.LayoutConfig;
import cn.devezhao.commons.web.ServletUtils;
/**
* 高级查询
*
@ -46,4 +51,13 @@ public class AdvFilterControll extends BaseControll implements LayoutConfig {
@Override
public void gets(String entity, HttpServletRequest request, HttpServletResponse response) throws IOException {
}
@RequestMapping("test-advfilter")
public void testAdvfilter(HttpServletRequest request, HttpServletResponse response) throws IOException {
JSON advfilter = ServletUtils.getRequestJson(request);
AdvFilterParser filterParser = new AdvFilterParser((JSONObject) advfilter);
String sql = filterParser.toSqlWhere();
writeSuccess(response, sql);
}
}

View file

@ -18127,7 +18127,6 @@ form.dropzone .dz-preview .dz-error-message {
outline: 0
}
.select2-container--default .select2-selection--single, .select2-container--default .select2-selection--multiple {
height: 37px
min-height: 37px;
font-size: 0;
}

View file

@ -6,6 +6,7 @@ class AdvFilter extends React.Component {
// TODO parse exists items
let items = []
this.state = { ...props, items: items }
this.handleChange = this.handleChange.bind(this)
this.childrenRef = []
}
render() {
@ -24,13 +25,13 @@ class AdvFilter extends React.Component {
<div className="adv-filter">
<div className="item">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input className="custom-control-input" type="checkbox" onClick={()=>this.toggleAdvexp()} />
<input className="custom-control-input" type="checkbox" onClick={()=>this.toggleEquation()} />
<span className="custom-control-label"> 启用高级表达式</span>
</label>
</div>
{this.state.enableAdvexp !== true ? null :
{this.state.enableEquation !== true ? null :
<div className="mb-3">
<input className="form-control form-control-sm form-control-success" ref="adv-exp" value={this.state.advexp} onChange={()=>this.handleChange()} />
<input className={'form-control form-control-sm' + (this.state.equationError ? ' is-invalid' : '')} value={this.state.equation || ''} onChange={this.handleChange} />
</div>
}
<div className="item">
@ -58,9 +59,22 @@ class AdvFilter extends React.Component {
onRef = (child) => {
this.childrenRef.push(child)
}
handleChange(event) {
let v = event.target.value
console.log(v)
handleChange(e) {
let val = e.target.value
let that = this
this.setState({ equation: val }, function(){
let token = val.toLowerCase().split(' ')
let hasError = false
for (let i = 0; i < token.length; i++) {
let t = $.trim(token[i])
if (!!!t) continue
if (!(t == '(' || t == ')' || t == 'or' || t == 'and' || (~~t > 0))) {
hasError = true
break
}
}
that.setState({ equationError: hasError })
})
}
addItem(){
@ -71,9 +85,9 @@ class AdvFilter extends React.Component {
let id = 'item-' + $random()
_items.push(<FilterItem index={_items.length + 1} fields={this.fields} $$$parent={this} key={id} id={id} onRef={this.onRef} />)
let advexp = []
for (let i = 1; i <= _items.length; i++) advexp.push(i)
this.setState({ items: _items, advexp: advexp.join(' OR ') })
let equation = []
for (let i = 1; i <= _items.length; i++) equation.push(i)
this.setState({ items: _items, equation: equation.join(' OR ') })
}
removeItem(id){
let _items = []
@ -87,17 +101,17 @@ class AdvFilter extends React.Component {
this.childrenRef = _children
let that = this
let advexp = []
for (let i = 1; i <= _items.length; i++) advexp.push(i)
this.setState({ items: _items, advexp: advexp.join(' OR ') }, ()=>{
let equation = []
for (let i = 1; i <= _items.length; i++) equation.push(i)
this.setState({ items: _items, equation: equation.join(' OR ') }, ()=>{
that.childrenRef.forEach((child, idx)=>{
child.setIndex(idx + 1)
})
})
}
toggleAdvexp() {
this.setState({ enableAdvexp: this.state.enableAdvexp !== true })
toggleEquation() {
this.setState({ enableEquation: this.state.enableEquation !== true })
}
toFilterJson() {
@ -111,8 +125,11 @@ class AdvFilter extends React.Component {
if (hasError){ rb.notice('部分条件设置有误,请检查'); return }
let adv = { entity: this.props.entity, items: filters }
if (this.state.enableAdvexp == true) adv.equation = this.state.advexp
console.log(JSON.stringify(adv))
if (this.state.enableEquation == true) adv.equation = this.state.equation
$.post(rb.baseUrl + '/app/entity/test-advfilter', JSON.stringify(adv), function(res){
console.log(JSON.stringify(adv) + ' >> ' + res.data)
})
}
}
@ -273,6 +290,9 @@ class FilterItem extends React.Component {
} else if (lastType == 'REFERENCE') {
this.removeBizzSearch()
}
if (this.state.value) this.valueCheck($(this.refs['filter-val']))
if (this.state.value2 && this.refs['filter-val2']) this.valueCheck($(this.refs['filter-val2']))
}
componentWillUnmount() {
this.__select2.forEach((item, index) => { item.select2('destroy') })
@ -282,20 +302,23 @@ class FilterItem extends React.Component {
this.removeBizzSearch()
}
valueHandle(event) {
valueHandle(e) {
let that = this
let val = event.target.value
if (event.target.dataset.at == 2) this.setState({ value2: val })
let val = e.target.value
if (e.target.dataset.at == 2) this.setState({ value2: val })
else this.setState({ value: val })
}
valueCheck(event){
let el = $(event.target)
let val = event.target.value
// @e = el or event
valueCheck(e){
let el = e.target ? $(e.target) : e
let val = e.target ? e.target.value : e.val()
if (!!!val){
el.addClass('is-invalid')
} else {
if (this.isNumberValue() && $regex.isDecimal(val) == false){
el.addClass('is-invalid')
} else if (this.state.type == 'DATE' && $regex.isUTCDate(val) == false) {
el.addClass('is-invalid')
} else {
el.removeClass('is-invalid')
}
@ -330,8 +353,7 @@ class FilterItem extends React.Component {
width: '100%',
}).on('change.select2', function(e){
let val = s2val.val()
that.setState({ value: val.join(',') }, function(){
})
that.setState({ value: val.join('|') })
})
this.__select2_PickList = s2val
}
@ -340,49 +362,7 @@ class FilterItem extends React.Component {
console.log('remove PickList ...')
this.__select2_PickList.select2('destroy')
this.__select2_PickList = null
}
}
renderDatepicker(){
console.log('render Datepicker ...')
let cfg = {
componentIcon:'zmdi zmdi-calendar',
navIcons: { rightIcon:'zmdi zmdi-chevron-right', leftIcon:'zmdi zmdi-chevron-left'},
format: 'yyyy-mm-dd',
minView: 2,
startView: 'month',
weekStart: 1,
autoclose: true,
language: 'zh',
todayHighlight: true,
showMeridian: false,
keyboardNavigation: false,
}
let that = this
let dp1 = $(this.refs['filter-val']).datetimepicker(cfg)
dp1.on('change.select2', function(e){
that.setState({ value: e.target.value }, function(){
})
})
this.__datepicker = [dp1]
if (this.refs['filter-val2']) {
let dp2 = $(this.refs['filter-val2']).datetimepicker(cfg)
dp2.on('change.select2', function(e){
that.setState({ value2: e.target.value }, function(){
})
})
this.__datepicker.push(dp2)
}
}
removeDatepicker(){
if (this.__datepicker) {
console.log('remove Datepicker ...')
this.__datepicker.forEach((item) => {
item.datetimepicker('remove')
})
this.__datepicker = null
this.setState({ value: null })
}
}
@ -411,7 +391,7 @@ class FilterItem extends React.Component {
}
}).on('change.select2', function(e){
let val = s2val.val()
that.setState({ value: val.join(',') })
that.setState({ value: val.join('|') })
})
this.__select2_BizzSearch = s2val
}
@ -420,6 +400,52 @@ class FilterItem extends React.Component {
console.log('remove BizzSearch ...')
this.__select2_BizzSearch.select2('destroy')
this.__select2_BizzSearch = null
this.setState({ value: null })
}
}
renderDatepicker(){
console.log('render Datepicker ...')
let cfg = {
componentIcon:'zmdi zmdi-calendar',
navIcons: { rightIcon:'zmdi zmdi-chevron-right', leftIcon:'zmdi zmdi-chevron-left'},
format: 'yyyy-mm-dd',
minView: 2,
startView: 'month',
weekStart: 1,
autoclose: true,
language: 'zh',
todayHighlight: true,
showMeridian: false,
keyboardNavigation: false,
}
let that = this
let dp1 = $(this.refs['filter-val']).datetimepicker(cfg)
dp1.on('change.select2', function(e){
that.setState({ value: e.target.value }, ()=>{
that.valueCheck($(that.refs['filter-val']))
})
})
this.__datepicker = [dp1]
if (this.refs['filter-val2']) {
let dp2 = $(this.refs['filter-val2']).datetimepicker(cfg)
dp2.on('change.select2', function(e){
that.setState({ value2: e.target.value }, ()=>{
that.valueCheck($(that.refs['filter-val2']))
})
})
this.__datepicker.push(dp2)
}
}
removeDatepicker(){
if (this.__datepicker) {
console.log('remove Datepicker ...')
this.__datepicker.forEach((item) => {
item.datetimepicker('remove')
})
this.__datepicker = null
}
}

View file

@ -146,6 +146,7 @@ const $cleanMap = function(map) {
// 常用正则
const $regex = {
_Date:/^((((1[6-9]|[2-9]\d)\d{2})-(0?[13578]|1[02])-(0?[1-9]|[12]\d|3[01]))|(((1[6-9]|[2-9]\d)\d{2})-(0?[13456789]|1[012])-(0?[1-9]|[12]\d|30))|(((1[6-9]|[2-9]\d)\d{2})-0?2-(0?[1-9]|1\d|2[0-8]))|(((1[6-9]|[2-9]\d)(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00))-0?2-29-))$/,
_UTCDate:/^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/, // 2010-01-01
_Url:/^(http|https|ftp)\:\/\/[a-z0-9\-\.]+(:[0-9]*)?\/?([a-z0-9\-\._\?\,\'\/\\\+&amp;%\$#\=~!:])*$/i,
_Mail:/^[a-z0-9._%-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i,
_Number:/^[-+]?[0-9]+$/, // 数字
@ -156,6 +157,9 @@ const $regex = {
isDate:function(val){
return this._Date.test(val);
},
isUTCDate:function(val){
return this._UTCDate.test(val);
},
isUrl:function(val){
return this._Url.test(val);
},