|
我正在尝试模拟sql语法来构建一个简单的sql类似于键值存储的接口。这些值本质上是POJO
) @, w* O! b, x6 q# f& e一个例子是- B3 r( ]& c. k5 ?3 [
select A.B.C from OBJ_POOL where A.B.X = 45 AND A.B.Y > '88' AND A.B.Z != 'abc';# U0 q% T* K, i1 F( e1 K5 U
OBJ_POOL只是相同类的POJO的列表。在此示例中,A将是基类。
; b% z3 @; n( l; bClass A5 m5 F5 I, w, Z) C) F5 E {
Class B
- I. u# S3 X3 d; Z/ r String C6 {$ Q3 C2 z5 r( ^2 `
Integer X
/ c7 N" E" o/ w _$ T String Y9 h) N8 `$ i) t1 A& i/ p ]
String Z7 S" S, X5 [6 I- m8 @# b
现在ABC等效于A.getB()。getC()3 E9 j% F2 X" P& P
我正在使用Antlr解析上述语句以获得AST,然后hoping使用Apache BeanUtils反射性地获取/设置字段名称。1 }$ S# V8 [) _/ c' t
我写了构建AST的语法 / J+ v9 {! d' s- b
" ]8 n" @* [/ H$ i& f4 u 现在我面临两个问题
4 q- [9 K! u4 a/ u& S- V) I: B[ol]where子句应如何实现访客?ABX = 45表示所有具有字段X为45的对象,请问如何进行过滤?+ f9 M9 e3 {. P( L7 p" q6 k9 H& J; |
有什么方法可以遍历所生成的AST,而不会用自定义逻辑(存储访问,属性获取器/设置器等)使访问者代码混乱。 9 h1 Z) @' A | a {
[/ol]1 u# s, I1 A: S! d
第二个问题更加令人担忧,因为该语句可能会做很多事情。& i1 S/ f8 h- f, c. V
简而言之,很好地解析sql select语句的一小部分的任何建议/链接/设计模式将不胜感激。
1 v2 J9 K' |5 H/ i8 W$ o0 `5 N谢谢5 Y, G$ s4 w, N" M
c6 ~! P* g$ q+ N2 S6 D) V解决方案:
7 c4 v. ^& v3 }8 p ( S# p! x% S' A- u' \* V
1 p4 s, i- ]- k0 z/ h- K& |0 O3 J+ W" A- _, H0 n9 q
您可以像我在博客文章中演示的那样进行操作(并且由于我知道您已阅读这些内容,因此我将不做详细介绍)。在这种情况下,唯一的区别是您的每一行数据都有其自己的范围。传递此范围的一种简单方法是将其作为eval(...)方法的参数提供。
3 s# p; c& Y- \# o$ Q y1 P; C以下是如何实现此功能的快速演示。请注意,我根据我的博客文章很快将它们一起破解了:并非所有功能都可用(请参阅很多功能TODO,并且其中也可能存在(小)错误,使用后果自负!)。
@% a) C% Z; x* [( U除了ANTLR v3.3,此演示还需要以下3个文件:
" x/ n0 J& t/ @8 E选择 Z/ ^* M7 @3 v s9 c- w$ U2 _
grammar Select;
9 p+ f, r& {( O5 moptions {
" s% O( k- P' g- r v. @% R output=AST;
$ o" B8 U9 \) a/ \}
+ J$ s* u9 i" V, Ltokens {
`1 B2 ?0 E7 J# p. k$ u // imaginary tokens$ h8 s& N- K [, U/ ?/ N6 h/ I
ROOT;
" S: x, ^+ @7 C/ `* N$ N1 O ATTR_LIST;
1 f* O7 d- R( P6 ]- b8 i% l1 s UNARY_MINUS;1 _- A8 m9 s5 u+ g
// literal tokens
# X" y0 k4 B4 W' j. t& a7 F; ] Eq = '='; W3 T+ R7 d2 Z' b; F, z0 R
NEq = '!=';
g, @. c: H" g: m LT = '';: H; ?. r0 C( B: e% v; Z
GTEq = '>=';6 q$ u& A( s9 D# s2 G5 H
Minus = '-';: }" P1 I7 d5 U0 _/ N
Not = '!';
$ A' Z. l/ s' P" }3 x Select = 'select';
& X1 F3 Y0 j+ s9 K' ` From = 'from';8 Q7 a5 x/ u: b [2 z: H8 |
Where = 'where'; \8 S6 h' j* p' S7 H4 ]& v
And = 'AND';+ W! M; F" s3 ^
Or = 'OR';+ `. X* U: [9 V6 R3 w
}1 h9 t# _6 j' M& U3 C+ Z' e
parse' `6 c5 t( i @0 Z9 I4 n8 Q
: select_stat EOF -> ^(ROOT select_stat)
" w8 s8 P1 m) k/ Y ;
1 M. P& b! o: ]select_stat
* F* E- f, V( r6 u: r! }& a6 X : Select attr_list From Id where_stat ';' -> ^(Select attr_list Id where_stat)) o, C U7 G2 c1 O% O+ Q& U# ]! Y
;, x: m, E; c, q
attr_list
7 m1 `* g9 \- t8 ^2 I* o : Id (',' Id)* -> ^(ATTR_LIST Id+)
: j: Q5 ^$ Y; T$ i ;9 c% C2 L) f# x4 G9 S& U& S
where_stat
K* z; x& W* h : Where expr -> expr
5 Q. ?+ A5 l+ A$ e' f4 {# ?3 k | -> ^(Eq Int["1"] Int["1"]) ! F# j* i3 R" [( H
// no 'where', insert '1=1' which is always true
" Y/ d* w5 y# ^6 B ;
* d9 F* m* f3 E( }2 w$ ~' {2 V4 Z5 }expr
% U' L; X3 `4 r7 Q Z : or_expr: x- \) x1 c' o$ \8 X5 @
;# P! H" A& \0 N
or_expr% r9 [+ L0 s3 x6 g* @
: and_expr (Or^ and_expr)*: J |0 z4 O4 K- y
;
z, c2 B! v0 D, nand_expr
7 e+ [7 a* l, c1 l3 O- R0 j : eq_expr (And^ eq_expr)*
$ O U3 ~4 v. E1 e! s4 t ;
9 e2 p4 u1 c& beq_expr2 r8 C- n$ E8 [# G8 c0 @
: rel_expr ((Eq | NEq)^ rel_expr)*2 H' c9 ~8 x8 }3 v' A
;
$ i7 O0 y. M# |/ y5 ~5 T; y8 F' H" Y* Hrel_expr
" ?6 V0 O4 K6 a* ` : unary_expr ((LT | LTEq | GT | GTEq)^ unary_expr)?
1 ~; B4 x& p% U( c8 `# @ ;( d- w2 h6 o$ W+ ` X9 c) w
unary_expr
3 L: l( R0 [, f' J$ }, r : Minus atom -> ^(UNARY_MINUS atom)
( _* _# i8 I) D! i9 m; s | Not atom -> ^(Not atom)7 C6 Q: X' g O# l
| atom: W$ }5 X( v# [' V
;/ R% J/ N3 r9 V. L; y
atom
! m+ ~1 S9 V' ] : Str: F$ U' o& J! p+ @9 X
| Int
6 x+ q* ]1 P+ _, ~" y d | Id
6 F, g9 G. N2 I- K7 j L, X& ^ | '(' expr ')' -> expr
& Y: @$ j t. }) [7 A ;; R% a3 N6 \8 k5 Z, y* ]
Id : ('a'..'z' | 'A'..'Z' | '_') ('a'..'z' | 'A'..'Z' | '_' | Digit)*;
! `. F+ B4 P+ |$ e2 PStr : '\'' ('\'\'' | ~('\'' | '\r' | '\n'))* '\''
( x# p d1 K9 z {
1 g+ [3 v( ]0 { // strip the surrounding quotes and replace '' with '
0 X2 I1 J$ ^- k. U5 r8 W setText($text.substring(1, $text.length() - 1).replace("''", "'"));
* d! G3 @* W, c% _; ^# \; ^ }
/ o& a/ c. W# Q8 S9 U3 e ;
% ]$ f G: i' q) zInt : Digit+;0 \( |4 `' w2 @* r7 B2 A$ k
Space : (' ' | '\t' | '\r' | '\n') {skip();};
7 p7 J0 j# k* v3 }fragment Digit : '0'..'9'; Q3 F5 I1 Q$ P1 {2 Q0 `3 F* I
SelectWalker.g
, J( P( \3 Y& @, ^( m- B& M3 ntree grammar SelectWalker;
3 D6 b+ U% N' [- C4 Yoptions {- o' `% V, h# A
tokenVocab=Select;3 t0 z0 X. f6 }* b0 E* E& w- ?
ASTLabelType=CommonTree;: I7 s7 r. ?! H/ ~
}; Q2 t+ z$ [4 ~2 Y% M
@header {7 _# T. D I* [4 p; ~% ~" _( g2 D% l
import java.util.List;
8 T, z- c7 r2 }. A import java.util.Map;
+ |, I; R1 `; n/ o, s( k! q import java.util.Set;9 G2 m$ O: y- r4 P, V6 Y$ z3 F
} u L- \7 h8 v/ g
@members {3 ]1 s4 |8 P, Y3 ]- J6 j
private Map dataPool;
( l- ?+ ]" B) w3 Y public SelectWalker(CommonTreeNodeStream nodes, Map data) {
+ w7 ]$ j' V) T! a+ m- l) K super(nodes);
9 T' v- l( C M: a1 h! W* U dataPool = data;4 N5 d2 m1 U1 S* H' |
}
# P0 D' C2 ^5 l u( |}: @) \, M* j. d( F; ~! i# k7 d- [
query returns [List> result]
' U; `4 v6 }1 {3 O2 |$ k- A1 n$ ?' Z : ^(ROOT select_stat) {$result = (List>)$select_stat.node.eval(null);}
1 n- q. N$ |3 b* J& k- m+ L ;5 j8 {* c, w8 c# {2 \" ^, X
select_stat returns [Node node]
v' Y, s5 Q2 o* o$ K! c8 _# S : ^(Select attr_list Id expr) . P) l. d$ ]+ c
{$node = new SelectNode($attr_list.attributes, dataPool.get($Id.text), $expr.node);}
4 O3 |" ?! Z! |6 q1 L' o6 S6 u ;( b5 K, W& w1 ~: M6 @! D
attr_list returns [List attributes]
& v: _9 B* m9 l6 D@init{$attributes = new ArrayList();}1 B3 a$ K# B/ {: x! z1 f
: ^(ATTR_LIST (Id {$attributes.add($Id.text);})+)4 B% T# |8 x# R9 |9 i/ w" } f# @
;
3 `$ o; x0 w: Z/ Yexpr returns [Node node]
3 z5 l/ S: g; S ^+ p4 f0 R& k | : ^(Or a=expr b=expr) {$node = null; /* TODO */}, X4 K2 y. E7 E) A6 E
| ^(And a=expr b=expr) {$node = new AndNode($a.node, $b.node);}' D3 F: M) A% {3 w; N) h( K
| ^(Eq a=expr b=expr) {$node = new EqNode($a.node, $b.node);}/ u# ^7 Q" q# `- j* M" e6 G3 n
| ^(NEq a=expr b=expr) {$node = new NEqNode($a.node, $b.node);}# H- r* H% s0 n
| ^(LT a=expr b=expr) {$node = null; /* TODO */}
& }# C0 i: X& e | ^(LTEq a=expr b=expr) {$node = null; /* TODO */}( w1 v# M; @8 ~9 {5 A+ c
| ^(GT a=expr b=expr) {$node = new GTNode($a.node, $b.node);}' _3 G: L( o7 L7 D0 i0 f6 X
| ^(GTEq a=expr b=expr) {$node = null; /* TODO */}
7 t, S) v( N0 X; W3 S9 x( W | ^(UNARY_MINUS a=expr) {$node = null; /* TODO */}! ?5 X( w; K1 `# N8 i% V9 p
| ^(Not a=expr) {$node = null; /* TODO */}
$ s/ u) M2 ]! N u0 e7 ^3 w | Str {$node = new AtomNode($Str.text);}
/ t0 X- ~! a( i: ^3 O# b8 d | Int {$node = new AtomNode(Integer.valueOf($Int.text));}
: F# c+ i. g8 `$ V/ ^8 f | | Id {$node = new IdNode($Id.text);}' r) Q& f8 t: B( d
;9 c0 V' A$ T4 q% A
Main.java: o/ Z8 p y3 Q$ K$ h. a
(是的,坚持所有这些Java类在同一个文件:Main.java)7 z7 v" x- S0 k2 p8 g
import org.antlr.runtime.*;
2 O3 E; C. i) _3 timport org.antlr.runtime.tree.*;; d8 s6 G$ V0 g, _
import org.antlr.stringtemplate.*;
) z' ?& }! m3 p" R2 `import java.util.*;' W8 R1 @7 y* P5 ?
public class Main {$ N& B6 Q+ H/ y: Z
static Map getData() {
8 K; m; _& d' Y! t9 s Map map = new HashMap();1 Z# E# B- h7 |6 k) ^
List[B] data = new ArrayList[B]();
3 [: `$ v. V, m8 t# f _) v, i- J data.add(new B("id_1", 345, "89", "abd"));! O: f" G9 e/ T- N, m
data.add(new B("id_2", 45, "89", "abd"));
; P# c4 O# u7 B! }! j data.add(new B("id_3", 1, "89", "abd"));3 _3 t: ~1 d: [2 i9 R% T e4 V
data.add(new B("id_4", 45, "8", "abd"));
( ]6 l6 u) d9 R' ]' E' k. M7 Y data.add(new B("id_5", 45, "89", "abc"));9 J; t. [1 U4 }# n$ Y
data.add(new B("id_6", 45, "99", "abC"));
: A$ |6 j. @0 b map.put("poolX", data);
& V) B; k8 k2 X/ }8 u return map;: E0 t6 t0 E( t$ E
}
0 ]" Y1 l. D! A* @( k+ B public static void main(String[] args) throws Exception {
6 w) E1 }! @/ b1 _ String src = "select C, Y from poolX where X = 45 AND Y > '88' AND Z != 'abc';";: U/ j5 N& u, V- O) T# R1 c
SelectLexer lexer = new SelectLexer(new ANTLRStringStream(src));& X. q! U% d! w
SelectParser parser = new SelectParser(new CommonTokenStream(lexer));: ]# o! I. E6 b8 |4 w8 z
CommonTree tree = (CommonTree)parser.parse().getTree(); ! S% l7 c5 M$ G' c3 C: ^& V1 B0 D @
SelectWalker walker = new SelectWalker(new CommonTreeNodeStream(tree), getData());
! W& C, L$ j+ ?* a6 w8 A% { List> result = walker.query();
, c3 V% V. C* ^& P for(List row : result) {
; e* i7 v4 q/ T [7 N2 m System.out.println(row);# _; C9 L' h" {# S; x7 o
}2 s0 b3 h9 ]9 M$ O
}) t/ n' q. [( f* R
}
2 Y, u8 a) c+ h" ?class B {# Y- N# c& q: S, o6 ~: d: M
String C;) I ^9 l- e* V; _9 e* F
Integer X;$ U: |" d: Z2 N* ~& } S y! B
String Y;
+ }# _, {; I- |1 | String Z;% G; @" V% k! t/ } |
B(String c, Integer x, String y, String z) {
2 i! Z5 g, a6 b3 F2 ~* J C = c;
8 C& Y; Z% d. [8 v/ A X = x;- |1 Q) @! n z! R b- D
Y = y;
: o9 D" J, }$ x) T g" x. c# \0 N+ e Z = z;
- Q5 ^/ o1 u# [ }6 c3 x7 W/ g- {- @# k5 k
Object getAttribute(String attribute) {
5 p9 ?" ?; G' n& T" \8 [ if(attribute.equals("C")) return C;
; V2 B1 W" l. @. ~0 m a+ }4 m if(attribute.equals("X")) return X;( Z) v! i+ x# _* F; }+ Z/ v
if(attribute.equals("Y")) return Y;
" l) A; N0 K% s9 O$ L9 Y. c& ~ if(attribute.equals("Z")) return Z;3 w$ Q& [% \ {& N! f: ?5 ]( j) |
throw new RuntimeException("Unknown attribute: B." + attribute);; ?7 _- m* X6 M K) |
// or use your Apache Bean-util API, or even reflection here instead of the above...
4 M; [! G+ \, E ^7 K) z }
! x, x8 X1 p& c F$ l- |# x}- }6 J$ |7 s/ x( y( x4 d4 {
interface Node {
8 s* v& y1 v( b Object eval(B b);$ f, }) L6 m$ k O# }$ Z9 k8 k% b
}
' @# S2 f8 y8 S& nclass AtomNode implements Node {
. |5 N* b" `+ C0 F t' Q. ]+ h final Object value;# _ Q* E4 G/ ^- c' A5 J3 D# J- l
AtomNode(Object v) {
6 S1 w' E4 w W5 v7 w5 ~+ r value = v;
- b0 c& w+ d: |/ k }- k* E! k* t4 E
public Object eval(B b) {
; ^6 X" K3 ^. q$ _1 ~, f% m return value;
' |0 c- s$ P$ v8 h }7 [6 W* d G3 t) t4 ]8 G8 C! v
}
! H7 }& k" q+ l- ]1 Q0 ^abstract class BinNode implements Node {# l* B2 p' m$ q; b* F3 A! O9 E
final Node left;# d5 L. g; g" k r
final Node right;
# [* L# d3 x* E BinNode(Node l, Node r) {
! \+ ^6 \! F3 x left = l;
' m4 m+ X& D7 Y% q; R right = r;, J( ?5 L5 C/ Q% L# T0 x& k1 k
}: x% E( @/ s# t" a; y
public abstract Object eval(B b);7 _ {& m& u6 ~
}
9 ^- F S6 J- F1 B2 O" sclass AndNode extends BinNode {
3 D: H! K1 b. Z5 }7 N AndNode(Node l, Node r) {, {% E) [2 o% n) h# s
super(l, r);3 j: ^9 O' v% ~5 l- G/ i
}* M0 i( Q* B- Q# q' [# W3 |
@Override
2 d/ y o' c q public Object eval(B b) {1 D7 E% y2 X. p) K: X) Y; n
return (Boolean)super.left.eval(b) && (Boolean)super.right.eval(b);: Q' x9 u* X3 `+ d
}
1 s% V8 y8 P8 H9 G" P. w1 i2 a}' [. T2 U& L. |0 X" s8 ?
class EqNode extends BinNode {
/ P2 o/ M% c5 g* b2 d* C EqNode(Node l, Node r) {* I8 ~2 c. R6 W' S0 m" z
super(l, r);
5 p5 ~4 o4 w. n* U) ~ }" V8 m/ N5 n. ~/ p
@Override
# h$ [; i1 x) q! f" I4 g public Object eval(B b) {
; H2 W- g- D* \2 n9 P# l return super.left.eval(b).equals(super.right.eval(b));
Y$ X/ z4 O2 s# b6 N }4 a: `: m1 p; G2 W* A2 V" J
}
# ?7 q4 M9 K% b' E4 p0 |4 Sclass NEqNode extends BinNode {- D: g d _+ F8 d
NEqNode(Node l, Node r) {
% @6 B+ D, n, ~, D0 y super(l, r);
3 [( r3 v; P! e }
% p' m1 D. }0 y+ ^: x @Override. U% B/ ^0 h) L$ N3 y. H) u% L/ G9 ?2 \
public Object eval(B b) {
. [, o$ u8 s/ x1 z$ S/ k return !super.left.eval(b).equals(super.right.eval(b));0 Q! q4 \! W: \) ~ D
}1 o4 W" W W+ D! o E& J
}
4 p; S) i3 m+ ]+ p) eclass GTNode extends BinNode {
3 }) i0 G+ B9 y1 j GTNode(Node l, Node r) {
7 {0 E! `# J& ` super(l, r);% s% g: y( a4 w( V2 h: A
}
9 M7 ]5 w; H, w; X @Override
/ b0 G" t; M7 z( a& c public Object eval(B b) {
: z& V, r6 k) `1 h) }& X" b0 | return ((Comparable)super.left.eval(b)).compareTo((Comparable)super.right.eval(b)) > 0;$ ^% q* u2 A. ~% m( Z
}& n. _( R I3 `/ Z
}* |, h @( E, [
class IdNode implements Node {
0 R1 y3 F; d; b: K final String id;
- o H: W: E8 U6 @ IdNode(String i) {2 m U2 J2 |' H5 }$ W
id = i;
. P$ l6 c$ G. s3 }) m# j: j3 g }
: c* d+ A% O9 v7 @! y @Override( R6 F8 [) e! s9 o* d4 e1 L
public Object eval(B b) {. Z$ v. Y# Z ^1 Q% Q5 E
return b.getAttribute(id);" t- S) \* f3 A8 x. y
}
! Z7 b' i7 A. \6 q" |}6 ], Q3 v/ x4 m
class SelectNode implements Node {, z& I" i2 O B+ K" C6 \# A5 Y
final List attributes;2 T8 R# r0 [0 @6 u$ N% r
final List[B] data;
7 M& P2 e* b+ e6 c% D8 l final Node expression;
5 D! M$ p) f; p/ i9 b7 g SelectNode(List a, List[B] d, Node e) {, Y" i; J* @; f# M- \* H
attributes = a;- [ t, l) |4 l& ?0 \
data = d;7 B/ h! ]# L5 i+ i
expression = e;
* i" `0 ~$ D: G" B3 J1 k: ` }
) G' |) B. C6 O1 |+ u( z4 V$ S @Override
0 B1 i4 W, z) A$ ^) h4 n public Object eval(B ignored) {" f" h- b X x/ ]6 r
List> result = new ArrayList>();' K, g- ]4 \8 ?2 C& j
for(B b : data) {
( [% r' R% t& o" ?$ [ if((Boolean)expression.eval(b)) {
! O+ u4 b; d. Z, M // 'b' passed, check which attributes to include+ _% z; h( R! S6 h( F
List row = new ArrayList();
+ Z( S/ v* \0 T6 D0 q" i for(String attr : attributes) {2 V7 |, h/ y3 Y! ^
row.add(b.getAttribute(attr));
) |& ^$ T" Q/ ]/ ?# P2 } }
5 K& V2 ]) Q+ E# h; p2 z. I9 O result.add(row);2 o) C4 ~! A) q, g9 Y
}
. B( ?; S4 l' d/ C' h8 r }
S: j( _; w, n, N3 Q6 Q0 y return result;3 o% O# C( X: d# ~1 E& |
}
+ X1 u4 Q0 v1 Y i}
/ j* A# T! |0 T: Y5 b* f' p; \如果现在生成词法分析器,解析器和tree walker并运行Main类:$ M3 e* _# S* I5 k, N+ D0 E0 |
java -cp antlr-3.3.jar org.antlr.Tool Select.g
/ ~8 W& ?8 D- j& O2 e: W* ]' Mjava -cp antlr-3.3.jar org.antlr.Tool SelectWalker.g
0 W9 Z) D A: R& Jjavac -cp antlr-3.3.jar *.java! B$ Z* B5 B! x1 p
java -cp .:antlr-3.3.jar Main' J7 N$ C/ B* m0 }$ J4 z6 @8 ]
您将看到查询的输出:
: e/ ^. t& H! _! p& c9 rselect C, Y from poolX where X = 45 AND Y > '88' AND Z != 'abc';
( Z3 f, G9 H; h输入:
" i8 s4 c6 E, H5 v) DC X Y Z
/ j4 l) l; e" t1 A4 |5 O"id_1" 345 "89" "abd"1 E" h8 |2 I7 v8 i+ Z
"id_2" 45 "89" "abd"- ]# w/ F. I0 C) E% l9 Z. D
"id_3" 1 "89" "abd"
( k* D$ ]3 t* [! X$ d V"id_4 45 "8" "abd"1 j2 h: ?& m! H7 l
"id_5" 45 "89" "abc"
- \7 C4 e4 S& `5 b; H5 y6 ["id_6" 45 "99" "abC"
$ k( B* @* ~9 P9 Z7 z是:/ ?1 E/ B3 [3 u8 b; a
[id_2, 89]
9 U2 \) e1 q$ T+ d[id_6, 99]( t7 b6 ^' M) g. |8 u; m
并注意,如果where省略该语句,1 = 1则会自动插入表达式,从而导致查询:: R4 B( W# B7 @ p2 C. A- e6 L
select C, Y from poolX;* c9 W( L3 |9 l5 c
打印以下内容:- L2 t( \1 M) e) D' \5 v
[id_1, 89], T$ b* F$ M- J$ P
[id_2, 89]
, @( o% {8 H" ?8 o[id_3, 89]( |# t2 N- T$ A) N% u3 G: P: q
[id_4, 8]
% B, P8 L$ @+ q1 ~3 k1 U$ M[id_5, 89]
: l% z- g( N3 D* j# Y[id_6, 99] |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|