diff --git a/src/main/java/com/rebuild/server/query/AdvFilterParser.java b/src/main/java/com/rebuild/server/query/AdvFilterParser.java index 61449fb95..a79e82b78 100644 --- a/src/main/java/com/rebuild/server/query/AdvFilterParser.java +++ b/src/main/java/com/rebuild/server/query/AdvFilterParser.java @@ -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 itemsSql = new ArrayList<>(); + Map 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 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 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 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 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 "''"; } /** diff --git a/src/main/java/com/rebuild/web/base/entity/AdvFilterControll.java b/src/main/java/com/rebuild/web/base/entity/AdvFilterControll.java index 0d53131af..2f3e3a1dc 100644 --- a/src/main/java/com/rebuild/web/base/entity/AdvFilterControll.java +++ b/src/main/java/com/rebuild/web/base/entity/AdvFilterControll.java @@ -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); + } } diff --git a/src/main/webapp/assets/css/rb-base.css b/src/main/webapp/assets/css/rb-base.css index 0bf1e0dbd..dcbd876d2 100644 --- a/src/main/webapp/assets/css/rb-base.css +++ b/src/main/webapp/assets/css/rb-base.css @@ -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; } diff --git a/src/main/webapp/assets/js/rb-advfilter.jsx b/src/main/webapp/assets/js/rb-advfilter.jsx index 7253c541a..635a54573 100644 --- a/src/main/webapp/assets/js/rb-advfilter.jsx +++ b/src/main/webapp/assets/js/rb-advfilter.jsx @@ -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 {
- {this.state.enableAdvexp !== true ? null : + {this.state.enableEquation !== true ? null :
- this.handleChange()} /> +
}
@@ -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() - 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 } } diff --git a/src/main/webapp/assets/js/rb-base.js b/src/main/webapp/assets/js/rb-base.js index 59dd58972..3413727c6 100644 --- a/src/main/webapp/assets/js/rb-base.js +++ b/src/main/webapp/assets/js/rb-base.js @@ -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\-\._\?\,\'\/\\\+&%\$#\=~!:])*$/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); },