root / tags / v1_1_Build_1014 / libraries / libGDBMS / src / main / java / com / hardcode / gdbms / engine / values / ValueFactory.java @ 13593
History | View | Annotate | Download (19.1 KB)
1 |
package com.hardcode.gdbms.engine.values; |
---|---|
2 |
|
3 |
import com.hardcode.gdbms.engine.instruction.SemanticException; |
4 |
import com.hardcode.gdbms.parser.SQLEngineConstants; |
5 |
|
6 |
import java.lang.reflect.Constructor; |
7 |
import java.lang.reflect.InvocationTargetException; |
8 |
import java.sql.Time; |
9 |
import java.sql.Timestamp; |
10 |
import java.sql.Types; |
11 |
|
12 |
import java.text.DateFormat; |
13 |
import java.text.ParseException; |
14 |
import java.text.SimpleDateFormat; |
15 |
|
16 |
import java.util.Date; |
17 |
import java.util.TimeZone; |
18 |
|
19 |
|
20 |
/**
|
21 |
* Factor?a abstracta de objetos value que dado un tipo b?sico, devuelve el
|
22 |
* wrapper apropiado
|
23 |
*
|
24 |
* @author $author$
|
25 |
* @version $Revision: 13593 $
|
26 |
*/
|
27 |
public class ValueFactory { |
28 |
final static int BYTE = 0; |
29 |
final static int SHORT = 1; |
30 |
final static int INTEGER = 2; |
31 |
final static int LONG = 3; |
32 |
final static int FLOAT = 5; |
33 |
final static int DOUBLE = 6; |
34 |
/**
|
35 |
* Crea un objeto de tipo Value a partir de un int
|
36 |
*
|
37 |
* @param n valor que se quiere representar
|
38 |
*
|
39 |
* @return objeto Value con el valor que se pasa como par?metro
|
40 |
*/
|
41 |
public static IntValue createValue(int n) { |
42 |
IntValue ret = new IntValue();
|
43 |
ret.setValue(n); |
44 |
|
45 |
return ret;
|
46 |
} |
47 |
|
48 |
/**
|
49 |
* Crea un objeto de tipo Value a partir de un long
|
50 |
*
|
51 |
* @param l valor que se quiere representar
|
52 |
*
|
53 |
* @return objeto Value con el valor que se pasa como par?metro
|
54 |
*/
|
55 |
public static LongValue createValue(long l) { |
56 |
LongValue ret = new LongValue();
|
57 |
ret.setValue(l); |
58 |
|
59 |
return ret;
|
60 |
} |
61 |
|
62 |
/**
|
63 |
* Crea un objeto de tipo Value a partir de un String
|
64 |
*
|
65 |
* @param s valor que se quiere representar
|
66 |
*
|
67 |
* @return objeto Value con el valor que se pasa como par?metro
|
68 |
*/
|
69 |
public static StringValue createValue(String s) { |
70 |
StringValue ret = new StringValue();
|
71 |
ret.setValue(s); |
72 |
|
73 |
return ret;
|
74 |
} |
75 |
|
76 |
/**
|
77 |
* Crea un objeto de tipo Value a partir de un float
|
78 |
*
|
79 |
* @param f valor que se quiere representar
|
80 |
*
|
81 |
* @return objeto Value con el valor que se pasa como par?metro
|
82 |
*/
|
83 |
public static FloatValue createValue(float f) { |
84 |
FloatValue ret = new FloatValue();
|
85 |
ret.setValue(f); |
86 |
|
87 |
return ret;
|
88 |
} |
89 |
|
90 |
/**
|
91 |
* Crea un objeto de tipo Value a partir de un double
|
92 |
*
|
93 |
* @param d valor que se quiere representar
|
94 |
*
|
95 |
* @return objeto Value con el valor que se pasa como par?metro
|
96 |
*/
|
97 |
public static DoubleValue createValue(double d) { |
98 |
DoubleValue ret = new DoubleValue();
|
99 |
ret.setValue(d); |
100 |
|
101 |
return ret;
|
102 |
} |
103 |
|
104 |
/**
|
105 |
* Crea un objeto de tipo Date a partir de un Date
|
106 |
*
|
107 |
* @param d valor que se quiere representar
|
108 |
*
|
109 |
* @return objeto Value con el valor que se pasa como par?metro
|
110 |
*/
|
111 |
public static DateValue createValue(Date d) { |
112 |
DateValue ret = new DateValue();
|
113 |
ret.setValue(d); |
114 |
|
115 |
return ret;
|
116 |
} |
117 |
|
118 |
/**
|
119 |
* Creates a TimeValue object
|
120 |
*
|
121 |
* @param t Time value
|
122 |
*
|
123 |
* @return TimeValue
|
124 |
*/
|
125 |
public static TimeValue createValue(Time t) { |
126 |
TimeValue ret = new TimeValue();
|
127 |
ret.setValue(t); |
128 |
|
129 |
return ret;
|
130 |
} |
131 |
|
132 |
/**
|
133 |
* Creates a TimestampValue object
|
134 |
*
|
135 |
* @param t Timestamp value
|
136 |
*
|
137 |
* @return TimestampValue
|
138 |
*/
|
139 |
public static TimestampValue createValue(Timestamp t) { |
140 |
TimestampValue ret = new TimestampValue();
|
141 |
ret.setValue(t); |
142 |
|
143 |
return ret;
|
144 |
} |
145 |
|
146 |
/**
|
147 |
* Crea un objeto de tipo Value a partir de un booleano
|
148 |
*
|
149 |
* @param b valor que se quiere representar
|
150 |
*
|
151 |
* @return objeto Value con el valor que se pasa como par?metro
|
152 |
*/
|
153 |
public static BooleanValue createValue(boolean b) { |
154 |
BooleanValue ret = new BooleanValue();
|
155 |
ret.setValue(b); |
156 |
|
157 |
return ret;
|
158 |
} |
159 |
|
160 |
/**
|
161 |
* Creates an ArrayValue
|
162 |
*
|
163 |
* @param values DOCUMENT ME!
|
164 |
*
|
165 |
* @return ArrayValue
|
166 |
*/
|
167 |
public static ValueCollection createValue(Value[] values) { |
168 |
ValueCollection v = new ValueCollection();
|
169 |
v.setValues(values); |
170 |
|
171 |
return v;
|
172 |
} |
173 |
|
174 |
/**
|
175 |
* Crea un Value a partir de un literal encontrado en una instrucci?n y su
|
176 |
* tipo
|
177 |
*
|
178 |
* @param text Texto del valor
|
179 |
* @param type Tipo del valor
|
180 |
*
|
181 |
* @return Objeto Value del tipo adecuado
|
182 |
*
|
183 |
* @throws SemanticException Si el tipo del literal no est? soportado
|
184 |
*/
|
185 |
public static Value createValue(String text, int type) |
186 |
throws SemanticException {
|
187 |
switch (type) {
|
188 |
case SQLEngineConstants.STRING_LITERAL:
|
189 |
|
190 |
StringValue r1 = new StringValue();
|
191 |
r1.setValue(text.substring(1, text.length() - 1)); |
192 |
|
193 |
return r1;
|
194 |
|
195 |
case SQLEngineConstants.INTEGER_LITERAL:
|
196 |
|
197 |
try {
|
198 |
IntValue r2 = new IntValue();
|
199 |
r2.setValue(Integer.parseInt(text));
|
200 |
|
201 |
return r2;
|
202 |
} catch (NumberFormatException e) { |
203 |
LongValue r2 = new LongValue();
|
204 |
r2.setValue(Long.parseLong(text));
|
205 |
|
206 |
return r2;
|
207 |
} |
208 |
|
209 |
case SQLEngineConstants.FLOATING_POINT_LITERAL:
|
210 |
|
211 |
try {
|
212 |
FloatValue r2 = new FloatValue();
|
213 |
r2.setValue(Float.parseFloat(text));
|
214 |
|
215 |
return r2;
|
216 |
} catch (NumberFormatException e) { |
217 |
DoubleValue r2 = new DoubleValue();
|
218 |
r2.setValue(Double.parseDouble(text));
|
219 |
|
220 |
return r2;
|
221 |
} |
222 |
|
223 |
default:
|
224 |
throw new SemanticException("Unexpected literal type: " + text + |
225 |
"->" + type);
|
226 |
} |
227 |
} |
228 |
|
229 |
/**
|
230 |
* DOCUMENT ME!
|
231 |
*
|
232 |
* @param text DOCUMENT ME!
|
233 |
* @param type DOCUMENT ME!
|
234 |
*
|
235 |
* @return DOCUMENT ME!
|
236 |
*
|
237 |
* @throws ParseException DOCUMENT ME!
|
238 |
*/
|
239 |
public static Value createValueByType(String text, int type) |
240 |
throws ParseException { |
241 |
Value value; |
242 |
|
243 |
switch (type) {
|
244 |
case Types.BIGINT: |
245 |
value = ValueFactory.createValue(Long.parseLong(text.trim()));
|
246 |
|
247 |
break;
|
248 |
|
249 |
case Types.BIT: |
250 |
case Types.BOOLEAN: |
251 |
value = ValueFactory.createValue(Boolean.valueOf(text.trim())
|
252 |
.booleanValue()); |
253 |
|
254 |
break;
|
255 |
|
256 |
case Types.CHAR: |
257 |
case Types.VARCHAR: |
258 |
case Types.LONGVARCHAR: |
259 |
value = ValueFactory.createValue(text); |
260 |
|
261 |
break;
|
262 |
|
263 |
case Types.DATE: |
264 |
try {
|
265 |
value = ValueFactory.createValue(new Date(Date.parse(text.trim()))); |
266 |
}catch (IllegalArgumentException e) { |
267 |
throw new ParseException(e.getMessage(),0); |
268 |
} |
269 |
break;
|
270 |
|
271 |
case Types.DECIMAL: |
272 |
case Types.NUMERIC: |
273 |
case Types.FLOAT: |
274 |
case Types.DOUBLE: |
275 |
value = ValueFactory.createValue(Double.parseDouble(text.trim()));
|
276 |
|
277 |
break;
|
278 |
|
279 |
case Types.INTEGER: |
280 |
value = ValueFactory.createValue(Integer.parseInt(text.trim()));
|
281 |
|
282 |
break;
|
283 |
|
284 |
case Types.REAL: |
285 |
value = ValueFactory.createValue(Float.parseFloat(text.trim()));
|
286 |
|
287 |
break;
|
288 |
|
289 |
case Types.SMALLINT: |
290 |
value = ValueFactory.createValue(Short.parseShort(text.trim()));
|
291 |
|
292 |
break;
|
293 |
|
294 |
case Types.TINYINT: |
295 |
value = ValueFactory.createValue(Byte.parseByte(text.trim()));
|
296 |
|
297 |
break;
|
298 |
|
299 |
case Types.BINARY: |
300 |
case Types.VARBINARY: |
301 |
case Types.LONGVARBINARY: |
302 |
|
303 |
if ((text.length() / 2) != (text.length() / 2.0)) { |
304 |
throw new ParseException("binary fields must have even number of characters.", |
305 |
0);
|
306 |
} |
307 |
|
308 |
byte[] array = new byte[text.length() / 2]; |
309 |
|
310 |
for (int i = 0; i < (text.length() / 2); i++) { |
311 |
String byte_ = text.substring(2 * i, (2 * i) + 2); |
312 |
array[i] = (byte) Integer.parseInt(byte_, 16); |
313 |
} |
314 |
|
315 |
value = ValueFactory.createValue(array); |
316 |
|
317 |
break;
|
318 |
|
319 |
case Types.TIMESTAMP: |
320 |
value = ValueFactory.createValue(Timestamp.valueOf(text.trim()));
|
321 |
|
322 |
break;
|
323 |
|
324 |
case Types.TIME: |
325 |
DateFormat tf = DateFormat.getTimeInstance(); |
326 |
value = ValueFactory.createValue(new Time( |
327 |
tf.parse(text.trim()).getTime())); |
328 |
|
329 |
break;
|
330 |
|
331 |
default:
|
332 |
value = ValueFactory.createValue(text); |
333 |
} |
334 |
|
335 |
return value;
|
336 |
} |
337 |
|
338 |
/**
|
339 |
* DOCUMENT ME!
|
340 |
*
|
341 |
* @param text DOCUMENT ME!
|
342 |
* @param className DOCUMENT ME!
|
343 |
*
|
344 |
* @return DOCUMENT ME!
|
345 |
*
|
346 |
* @throws SemanticException DOCUMENT ME!
|
347 |
*
|
348 |
* @deprecated Use createValueWithType(String, int) instead
|
349 |
*/
|
350 |
public static Value createValue(String text, String className) |
351 |
throws SemanticException {
|
352 |
if (className.equals("com.hardcode.gdbms.engine.values.BooleanValue")) { |
353 |
return createValue(Boolean.getBoolean(text)); |
354 |
} |
355 |
|
356 |
if (className.equals("com.hardcode.gdbms.engine.values.DateValue")) { |
357 |
try {
|
358 |
return createValue(DateFormat.getInstance().parse(text)); |
359 |
} catch (ParseException e) { |
360 |
throw new SemanticException(e); |
361 |
} |
362 |
} |
363 |
|
364 |
if (className.equals("com.hardcode.gdbms.engine.values.DoubleValue")) { |
365 |
return createValue(Double.parseDouble(text)); |
366 |
} |
367 |
|
368 |
if (className.equals("com.hardcode.gdbms.engine.values.FloatValue")) { |
369 |
return createValue(Float.parseFloat(text)); |
370 |
} |
371 |
|
372 |
if (className.equals("com.hardcode.gdbms.engine.values.IntValue")) { |
373 |
return createValue(Integer.parseInt(text)); |
374 |
} |
375 |
|
376 |
if (className.equals("com.hardcode.gdbms.engine.values.LongValue")) { |
377 |
return createValue(Long.parseLong(text)); |
378 |
} |
379 |
|
380 |
if (className.equals("com.hardcode.gdbms.engine.values.StringValue")) { |
381 |
return createValue(text);
|
382 |
} |
383 |
|
384 |
// default:
|
385 |
throw new SemanticException( |
386 |
"Unexpected className in createValue (GDBMS) text: " + text +
|
387 |
"-> className: " + className);
|
388 |
} |
389 |
|
390 |
/**
|
391 |
* Creates a new null Value
|
392 |
*
|
393 |
* @return NullValue
|
394 |
*/
|
395 |
public static NullValue createNullValue() { |
396 |
return new NullValue(); |
397 |
} |
398 |
|
399 |
/**
|
400 |
* Dado el tipo que se pasa como par?metro, expresado con una de las
|
401 |
* constantes definidas en la clase java.sql.Types se devuelve la clase
|
402 |
* que implementa dicho tipo
|
403 |
*
|
404 |
* @param type Tipo de la columna
|
405 |
*
|
406 |
* @return Clase que implementa el tipo
|
407 |
*
|
408 |
* @throws RuntimeException if type is not recognized
|
409 |
*/
|
410 |
public static Class getType(int type) { |
411 |
switch (type) {
|
412 |
case Types.NUMERIC: |
413 |
case Types.BIGINT: |
414 |
return LongValue.class;
|
415 |
|
416 |
case Types.BIT: |
417 |
return BooleanValue.class;
|
418 |
|
419 |
case Types.SMALLINT: |
420 |
return ShortValue.class;
|
421 |
|
422 |
case Types.TINYINT: |
423 |
return ByteValue.class;
|
424 |
|
425 |
case Types.CHAR: |
426 |
case Types.VARCHAR: |
427 |
case Types.LONGVARCHAR: |
428 |
return StringValue.class;
|
429 |
|
430 |
case Types.DATE: |
431 |
return DateValue.class;
|
432 |
|
433 |
case Types.FLOAT: |
434 |
case Types.DOUBLE: |
435 |
case Types.DECIMAL: |
436 |
return DoubleValue.class;
|
437 |
|
438 |
case Types.INTEGER: |
439 |
return IntValue.class;
|
440 |
|
441 |
case Types.REAL: |
442 |
return FloatValue.class;
|
443 |
|
444 |
case Types.BINARY: |
445 |
case Types.VARBINARY: |
446 |
case Types.LONGVARBINARY: |
447 |
return BinaryValue.class;
|
448 |
|
449 |
case Types.TIMESTAMP: |
450 |
return TimestampValue.class;
|
451 |
|
452 |
case Types.TIME: |
453 |
return TimeValue.class;
|
454 |
|
455 |
case Types.OTHER:default: |
456 |
throw new RuntimeException("Type not recognized: " + type); |
457 |
} |
458 |
} |
459 |
|
460 |
/**
|
461 |
* Gets a Value with the value v1 plus v2
|
462 |
*
|
463 |
* @param v1 first value
|
464 |
* @param v2 second value
|
465 |
*
|
466 |
* @return a numeric value with the operation
|
467 |
*/
|
468 |
static NumericValue suma(NumericValue v1, NumericValue v2) {
|
469 |
int type = Math.max(v1.getType(), v2.getType()); |
470 |
|
471 |
while (true) { |
472 |
switch (type) {
|
473 |
/*
|
474 |
* El operador '+' en java no est? definido para byte ni short, as?
|
475 |
* que nosotros tampoco lo definimos.
|
476 |
* Por otro lado no conocemos manera de detectar el overflow al operar
|
477 |
* con long's ni double's de manera eficiente, as? que no se detecta.
|
478 |
*/
|
479 |
case BYTE:
|
480 |
case SHORT:
|
481 |
case INTEGER:
|
482 |
|
483 |
int intValue = v1.intValue() + v2.intValue();
|
484 |
|
485 |
if ((intValue) != (v1.longValue() + v2.longValue())) {
|
486 |
type = LONG; |
487 |
|
488 |
continue;
|
489 |
} else {
|
490 |
return (NumericValue) createValue(intValue);
|
491 |
} |
492 |
|
493 |
case LONG:
|
494 |
return (NumericValue) createValue(v1.longValue() +
|
495 |
v2.longValue()); |
496 |
|
497 |
case FLOAT:
|
498 |
|
499 |
float floatValue = v1.floatValue() + v2.floatValue();
|
500 |
|
501 |
if ((floatValue) != (v1.doubleValue() + v2.doubleValue())) {
|
502 |
type = DOUBLE; |
503 |
|
504 |
continue;
|
505 |
} else {
|
506 |
return (NumericValue) createValue(floatValue);
|
507 |
} |
508 |
|
509 |
case DOUBLE:
|
510 |
return (NumericValue) createValue(v1.doubleValue() +
|
511 |
v2.doubleValue()); |
512 |
} |
513 |
} |
514 |
} |
515 |
|
516 |
/**
|
517 |
* Gets the value of the operation v1 v2
|
518 |
*
|
519 |
* @param v1 first value
|
520 |
* @param v2 second value
|
521 |
*
|
522 |
* @return a numeric value with the operation
|
523 |
*/
|
524 |
static NumericValue producto(NumericValue v1, NumericValue v2) {
|
525 |
int type = Math.max(v1.getType(), v2.getType()); |
526 |
|
527 |
while (true) { |
528 |
switch (type) {
|
529 |
/*
|
530 |
* El operador '+' en java no est? definido para byte ni short, as?
|
531 |
* que nosotros tampoco lo definimos.
|
532 |
* Por otro lado no conocemos manera de detectar el overflow al operar
|
533 |
* con long's ni double's de manera eficiente, as? que no se detecta.
|
534 |
*/
|
535 |
case BYTE:
|
536 |
case SHORT:
|
537 |
case INTEGER:
|
538 |
|
539 |
int intValue = v1.intValue() * v2.intValue();
|
540 |
|
541 |
if ((intValue) != (v1.intValue() * v2.intValue())) {
|
542 |
type = LONG; |
543 |
|
544 |
continue;
|
545 |
} else {
|
546 |
return (NumericValue) createValue(intValue);
|
547 |
} |
548 |
|
549 |
case LONG:
|
550 |
return (NumericValue) createValue(v1.longValue() * v2.longValue());
|
551 |
|
552 |
case FLOAT:
|
553 |
|
554 |
float floatValue = v1.floatValue() * v2.floatValue();
|
555 |
|
556 |
if ((floatValue) != (v1.doubleValue() * v2.doubleValue())) {
|
557 |
type = DOUBLE; |
558 |
|
559 |
continue;
|
560 |
} else {
|
561 |
return (NumericValue) createValue(floatValue);
|
562 |
} |
563 |
|
564 |
case DOUBLE:
|
565 |
return (NumericValue) createValue(v1.doubleValue() * v2.doubleValue());
|
566 |
} |
567 |
} |
568 |
} |
569 |
|
570 |
/**
|
571 |
* Calcula la inversa (1/v) del valor que se pasa como par?metro.
|
572 |
*
|
573 |
* @param v Valor cuya inversa se quiere obtener
|
574 |
*
|
575 |
* @return DoubleValue
|
576 |
*/
|
577 |
static NumericValue inversa(NumericValue v) {
|
578 |
int type = v.getType();
|
579 |
|
580 |
return (NumericValue) createValue(1 / v.doubleValue()); |
581 |
} |
582 |
|
583 |
/**
|
584 |
* Creates a byte array value
|
585 |
*
|
586 |
* @param bytes bytes of the value
|
587 |
*
|
588 |
* @return
|
589 |
*/
|
590 |
public static BinaryValue createValue(byte[] bytes) { |
591 |
BinaryValue ret = new BinaryValue(bytes);
|
592 |
|
593 |
return ret;
|
594 |
} |
595 |
|
596 |
/**
|
597 |
* Creates a complex value based on a xml parseable
|
598 |
* string
|
599 |
*
|
600 |
* @param string
|
601 |
*
|
602 |
* @return new instance of ComplexValue or null???
|
603 |
*/
|
604 |
public static ComplexValue createComplexValue(String string) { |
605 |
try {
|
606 |
return new ComplexValue(string); |
607 |
|
608 |
} catch (Exception e) { |
609 |
//FIXME: OJO!!! QUE HACEMOS AQUI
|
610 |
return null; |
611 |
} |
612 |
} |
613 |
|
614 |
/**
|
615 |
* Create a Value instance from a String value using
|
616 |
* the class specified in valueName.
|
617 |
* <P>
|
618 |
*
|
619 |
* <code>
|
620 |
* Nota: Habria que ver que hacemos con esto... seguimos delegando
|
621 |
* o modificar el createValueByType para que delege en esta
|
622 |
* </code>
|
623 |
* <P>
|
624 |
* @param text String representation of value
|
625 |
* @param valueName class name of the instance of Value to return.
|
626 |
* It can be the class name without the package.
|
627 |
* @return a Value instance
|
628 |
* @throws ParseException
|
629 |
*/
|
630 |
public static Value createValueByValueName(String text, String valueName) |
631 |
throws ParseException { |
632 |
String baseName;
|
633 |
if (valueName.indexOf(".") > -1) { |
634 |
baseName = valueName.substring( valueName.lastIndexOf(".")+1); |
635 |
} else {
|
636 |
baseName = valueName; |
637 |
} |
638 |
if (baseName.equals("BinaryValue")) { |
639 |
return createValueByType(text,Types.BINARY); |
640 |
} else if (baseName.equals("BooleanValue")) { |
641 |
return createValueByType(text,Types.BOOLEAN); |
642 |
} else if (baseName.equals("ByteValue")) { |
643 |
return createValueByType(text,Types.TINYINT); |
644 |
} else if (baseName.equals("ComplexValue")) { |
645 |
return createComplexValue(text);
|
646 |
} else if (baseName.equals("DateValue")) { |
647 |
return createValueByType(text,Types.DATE); |
648 |
} else if (baseName.equals("DoubleValue")) { |
649 |
return createValueByType(text,Types.DOUBLE); |
650 |
} else if (baseName.equals("FloatValue")) { |
651 |
return createValueByType(text,Types.REAL); |
652 |
} else if (baseName.equals("IntValue")) { |
653 |
return createValueByType(text,Types.INTEGER); |
654 |
} else if (baseName.equals("LongValue")) { |
655 |
return createValueByType(text,Types.BIGINT); |
656 |
} else if (baseName.equals("NullValue")) { |
657 |
return new NullValue(); |
658 |
} else if (baseName.equals("ShortValue")) { |
659 |
return createValueByType(text,Types.SMALLINT); |
660 |
} else if (baseName.equals("StringValue")) { |
661 |
return createValueByType(text,Types.VARCHAR); |
662 |
} else if (baseName.equals("TimestampValue")) { |
663 |
return createValueByType(text,Types.TIMESTAMP); |
664 |
} else if (baseName.equals("TimeValue")) { |
665 |
return createValueByType(text,Types.TIME); |
666 |
} else {
|
667 |
try {
|
668 |
Class valueClass = Class.forName(valueName); |
669 |
Constructor constr= valueClass.getConstructor(new Class[] {String.class}); |
670 |
return (Value) constr.newInstance(new Object[] {text}); |
671 |
} catch (Exception e) { |
672 |
throw new ParseException(e.getMessage(),0); |
673 |
} |
674 |
} |
675 |
} |
676 |
} |