Revision 44951 trunk/org.gvsig.desktop/org.gvsig.desktop.compat.cdc/org.gvsig.fmap.dal/org.gvsig.fmap.dal.db/org.gvsig.fmap.dal.db.mdb/src/main/java/org/gvsig/fmap/dal/store/mdb/MDBSQLBuilder.java
MDBSQLBuilder.java | ||
---|---|---|
2 | 2 |
|
3 | 3 |
import java.sql.PreparedStatement; |
4 | 4 |
import java.sql.SQLException; |
5 |
import java.sql.Timestamp; |
|
5 | 6 |
import java.text.MessageFormat; |
6 | 7 |
import java.util.ArrayList; |
7 | 8 |
import java.util.Date; |
8 | 9 |
import java.util.List; |
9 | 10 |
import java.util.Objects; |
10 | 11 |
import org.apache.commons.lang3.tuple.Pair; |
12 |
import org.gvsig.expressionevaluator.ExpressionBuilder; |
|
13 |
import static org.gvsig.expressionevaluator.ExpressionBuilder.PARAMETER_TYPE_CONSTANT; |
|
14 |
import static org.gvsig.expressionevaluator.ExpressionBuilder.PARAMETER_TYPE_VARIABLE; |
|
11 | 15 |
import org.gvsig.expressionevaluator.ExpressionBuilder.Parameter; |
12 | 16 |
import org.gvsig.expressionevaluator.Formatter; |
17 |
import org.gvsig.expressionevaluator.GeometryExpressionBuilderHelper; |
|
18 |
import static org.gvsig.expressionevaluator.GeometryExpressionBuilderHelper.PARAMETER_TYPE_GEOMETRY; |
|
13 | 19 |
import org.gvsig.fmap.dal.DataTypes; |
14 | 20 |
import org.gvsig.fmap.dal.feature.FeatureAttributeDescriptor; |
21 |
import org.gvsig.fmap.dal.feature.FeatureReference; |
|
15 | 22 |
import org.gvsig.fmap.dal.feature.FeatureType; |
16 | 23 |
import org.gvsig.fmap.dal.feature.spi.FeatureProvider; |
17 |
import org.gvsig.fmap.dal.store.mdb.expressionbuilderformatter.MDBSpatialFormatter;
|
|
24 |
import org.gvsig.fmap.dal.store.mdb.expressionbuilderformatter.MDBFormatter; |
|
18 | 25 |
import org.gvsig.fmap.dal.store.jdbc2.spi.JDBCSQLBuilderBase; |
19 | 26 |
import org.gvsig.fmap.dal.store.mdb.MDBHelper; |
20 | 27 |
import org.gvsig.fmap.geom.Geometry; |
... | ... | |
28 | 35 |
public MDBSQLBuilder(MDBHelper helper) { |
29 | 36 |
super(helper); |
30 | 37 |
|
31 |
this.defaultSchema = "PUBLIC";
|
|
32 |
this.supportSchemas = true;
|
|
38 |
this.defaultSchema = ""; |
|
39 |
this.supportSchemas = false;
|
|
33 | 40 |
this.allowAutomaticValues = true; |
34 | 41 |
this.geometrySupportType = this.helper.getGeometrySupportType(); |
35 | 42 |
this.hasSpatialFunctions = this.helper.hasSpatialFunctions(); |
... | ... | |
44 | 51 |
this.type_bytearray = "BLOB"; |
45 | 52 |
this.type_geometry = "GEOMETRY"; |
46 | 53 |
this.type_char = "CHAR"; |
47 |
this.type_date = "DATE"; |
|
48 |
this.type_double = "DOUBLE";
|
|
54 |
this.type_date = "DATETIME";
|
|
55 |
this.type_double = "FLOAT";
|
|
49 | 56 |
this.type_decimal_ps = "DECIMAL({0,Number,##########},{1,Number,##########})"; |
50 | 57 |
this.type_decimal_p = "DECIMAL({0,Number,##########})"; |
51 | 58 |
this.type_float = "REAL"; |
52 | 59 |
this.type_int = "INTEGER"; |
53 |
this.type_long = "BIGINT";
|
|
60 |
this.type_long = "DECIMAL(19,0)";
|
|
54 | 61 |
this.type_string = "VARCHAR"; |
55 | 62 |
this.type_string_p = "VARCHAR({0,Number,##########})"; |
56 |
this.type_time = "TIME"; |
|
57 |
this.type_timestamp = "TIMESTAMP";
|
|
63 |
this.type_time = "DATETIME";
|
|
64 |
this.type_timestamp = "DATETIME";
|
|
58 | 65 |
this.type_version = "VARCHAR"; |
59 | 66 |
this.type_URI = "VARCHAR"; |
60 | 67 |
this.type_URL = "VARCHAR"; |
... | ... | |
65 | 72 |
@Override |
66 | 73 |
protected Formatter formatter() { |
67 | 74 |
if( this.formatter==null ) { |
68 |
this.formatter = new MDBSpatialFormatter(this);
|
|
75 |
this.formatter = new MDBFormatter(this); |
|
69 | 76 |
} |
70 | 77 |
return this.formatter; |
71 | 78 |
} |
... | ... | |
172 | 179 |
} |
173 | 180 |
sqls.add(builder.toString()); |
174 | 181 |
|
175 |
if( column.isGeometry() ) { |
|
176 |
String constraint_name = "constraint_" + this.table().getName() + "_" + column.getName()+"_geom"; |
|
177 |
String sql; |
|
178 |
if( column.getGeometrySRSId()==null ) { |
|
179 |
sql = MessageFormat.format( |
|
180 |
"ALTER TABLE \"{0}\".\"{1}\" ADD CONSTRAINT IF NOT EXISTS \"{2}\" CHECK NVL2(\"{3}\", ST_GeometryTypeCode(\"{3}\") = {4,number,###} AND ST_CoordDim(\"{3}\") = {5,number,###}, TRUE)", |
|
181 |
this.table().getSchema(), |
|
182 |
this.table().getName(), |
|
183 |
constraint_name, |
|
184 |
column.getName(), |
|
185 |
sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()), |
|
186 |
sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()), |
|
187 |
column.getGeometrySRSId() |
|
188 |
); |
|
189 |
} else { |
|
190 |
sql = MessageFormat.format( |
|
191 |
"ALTER TABLE \"{0}\".\"{1}\" ADD CONSTRAINT IF NOT EXISTS \"{2}\" CHECK NVL2(\"{3}\", ST_GeometryTypeCode(\"{3}\") = {4,number,###} AND ST_CoordDim(\"{3}\") = {5,number,###} AND ST_SRID(\"{3}\") = {6,number,#####}, TRUE)", |
|
192 |
this.table().getSchema(), |
|
193 |
this.table().getName(), |
|
194 |
constraint_name, |
|
195 |
column.getName(), |
|
196 |
sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()), |
|
197 |
sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()), |
|
198 |
column.getGeometrySRSId() |
|
199 |
); |
|
200 |
} |
|
201 |
sqls.add(sql); |
|
202 |
} |
|
182 |
// if( column.isGeometry() ) {
|
|
183 |
// String constraint_name = "constraint_" + this.table().getName() + "_" + column.getName()+"_geom";
|
|
184 |
// String sql;
|
|
185 |
// if( column.getGeometrySRSId()==null ) {
|
|
186 |
// sql = MessageFormat.format(
|
|
187 |
// "ALTER TABLE \"{0}\".\"{1}\" ADD CONSTRAINT IF NOT EXISTS \"{2}\" CHECK NVL2(\"{3}\", ST_GeometryTypeCode(\"{3}\") = {4,number,###} AND ST_CoordDim(\"{3}\") = {5,number,###}, TRUE)",
|
|
188 |
// this.table().getSchema(),
|
|
189 |
// this.table().getName(),
|
|
190 |
// constraint_name,
|
|
191 |
// column.getName(),
|
|
192 |
// sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()),
|
|
193 |
// sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()),
|
|
194 |
// column.getGeometrySRSId()
|
|
195 |
// );
|
|
196 |
// } else {
|
|
197 |
// sql = MessageFormat.format(
|
|
198 |
// "ALTER TABLE \"{0}\".\"{1}\" ADD CONSTRAINT IF NOT EXISTS \"{2}\" CHECK NVL2(\"{3}\", ST_GeometryTypeCode(\"{3}\") = {4,number,###} AND ST_CoordDim(\"{3}\") = {5,number,###} AND ST_SRID(\"{3}\") = {6,number,#####}, TRUE)",
|
|
199 |
// this.table().getSchema(),
|
|
200 |
// this.table().getName(),
|
|
201 |
// constraint_name,
|
|
202 |
// column.getName(),
|
|
203 |
// sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()),
|
|
204 |
// sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()),
|
|
205 |
// column.getGeometrySRSId()
|
|
206 |
// );
|
|
207 |
// }
|
|
208 |
// sqls.add(sql);
|
|
209 |
// }
|
|
203 | 210 |
} |
204 | 211 |
for (ColumnDescriptor column : alters) { |
205 | 212 |
StringBuilder builder = new StringBuilder(); |
... | ... | |
231 | 238 |
builder.append(" AUTO_INCREMENT"); |
232 | 239 |
} |
233 | 240 |
sqls.add(builder.toString()); |
234 |
if( column.isGeometry() ) { |
|
235 |
String sql; |
|
236 |
String constraint_name = "constraint_" + this.table().getName() + "_" + column.getName()+"_dim"; |
|
237 |
sql = MessageFormat.format( |
|
238 |
"ALTER TABLE \"{0}\".\"{1}\" ADD CONSTRAINT IF NOT EXISTS \"{2}\" CHECK ST_CoordDim(\"{3}\") = {4}", |
|
239 |
this.table().getSchema(), |
|
240 |
this.table().getName(), |
|
241 |
constraint_name, |
|
242 |
column.getName(), |
|
243 |
sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()) |
|
244 |
); |
|
245 |
sqls.add(sql); |
|
246 |
} |
|
241 |
// if( column.isGeometry() ) {
|
|
242 |
// String sql;
|
|
243 |
// String constraint_name = "constraint_" + this.table().getName() + "_" + column.getName()+"_dim";
|
|
244 |
// sql = MessageFormat.format(
|
|
245 |
// "ALTER TABLE \"{0}\".\"{1}\" ADD CONSTRAINT IF NOT EXISTS \"{2}\" CHECK ST_CoordDim(\"{3}\") = {4}",
|
|
246 |
// this.table().getSchema(),
|
|
247 |
// this.table().getName(),
|
|
248 |
// constraint_name,
|
|
249 |
// column.getName(),
|
|
250 |
// sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype())
|
|
251 |
// );
|
|
252 |
// sqls.add(sql);
|
|
253 |
// }
|
|
247 | 254 |
} |
248 | 255 |
for (Pair<String,String> pair : renames) { |
249 | 256 |
StringBuilder builder = new StringBuilder(); |
... | ... | |
259 | 266 |
} |
260 | 267 |
|
261 | 268 |
} |
262 |
|
|
269 |
|
|
263 | 270 |
protected class MDBCreateTableBuilder extends CreateTableBuilderBase { |
264 | 271 |
|
265 | 272 |
@Override |
... | ... | |
273 | 280 |
builder.append(" ("); |
274 | 281 |
boolean first = true; |
275 | 282 |
for (ColumnDescriptor column : columns) { |
283 |
if( column.isGeometry() ) { |
|
284 |
continue; |
|
285 |
} |
|
276 | 286 |
|
277 | 287 |
if (first) { |
278 | 288 |
first = false; |
... | ... | |
292 | 302 |
) |
293 | 303 |
); |
294 | 304 |
if( column.isGeometry() ) { |
295 |
// |
|
296 |
// https://github.com/orbisgis/h2gis/wiki/1.-Spatial-data#geometry-columns-view |
|
297 |
// https://github.com/orbisgis/h2gis/blob/master/h2gis-functions/src/main/java/org/h2gis/functions/spatial/type/GeometryTypeFromConstraint.java |
|
298 |
// |
|
299 |
if( column.getGeometrySRSId()==null ) { |
|
300 |
builder.append( |
|
301 |
MessageFormat.format( |
|
302 |
" CHECK NVL2(\"{0}\", ST_GeometryTypeCode(\"{0}\") = {1} AND ST_CoordDim(\"{0}\") = {2}, TRUE)", |
|
303 |
column.getName(), |
|
304 |
sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()), |
|
305 |
sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()) ) |
|
306 |
); |
|
307 |
} else { |
|
308 |
builder.append( |
|
309 |
MessageFormat.format( |
|
310 |
" CHECK NVL2(\"{0}\", ST_GeometryTypeCode(\"{0}\") = {1,number,###} AND ST_CoordDim(\"{0}\") = {2,number,###} AND ST_SRID(\"{0}\") = {3,number,#####}, TRUE)", |
|
311 |
column.getName(), |
|
312 |
sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()), |
|
313 |
sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()), |
|
314 |
column.getGeometrySRSId() |
|
315 |
) |
|
316 |
); |
|
317 |
} |
|
305 |
// // |
|
306 |
// // https://github.com/orbisgis/h2gis/wiki/1.-Spatial-data#geometry-columns-view |
|
307 |
// // https://github.com/orbisgis/h2gis/blob/master/h2gis-functions/src/main/java/org/h2gis/functions/spatial/type/GeometryTypeFromConstraint.java |
|
308 |
// // |
|
309 |
// if( column.getGeometrySRSId()==null ) { |
|
310 |
// builder.append( |
|
311 |
// MessageFormat.format( |
|
312 |
// " CHECK NVL2(\"{0}\", ST_GeometryTypeCode(\"{0}\") = {1} AND ST_CoordDim(\"{0}\") = {2}, TRUE)", |
|
313 |
// column.getName(), |
|
314 |
// sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()), |
|
315 |
// sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()) ) |
|
316 |
// ); |
|
317 |
// } else { |
|
318 |
// builder.append( |
|
319 |
// MessageFormat.format( |
|
320 |
// " CHECK NVL2(\"{0}\", ST_GeometryTypeCode(\"{0}\") = {1,number,###} AND ST_CoordDim(\"{0}\") = {2,number,###} AND ST_SRID(\"{0}\") = {3,number,#####}, TRUE)", |
|
321 |
// column.getName(), |
|
322 |
// sqlgeometrytype(column.getGeometryType(),column.getGeometrySubtype()), |
|
323 |
// sqlgeometrynumdimension(column.getGeometryType(),column.getGeometrySubtype()), |
|
324 |
// column.getGeometrySRSId() |
|
325 |
// ) |
|
326 |
// ); |
|
327 |
// } |
|
328 |
|
|
318 | 329 |
} else { |
319 | 330 |
if (column.isPrimaryKey()) { |
320 | 331 |
builder.append(" PRIMARY KEY"); |
... | ... | |
352 | 363 |
return sqls; |
353 | 364 |
} |
354 | 365 |
} |
355 |
|
|
366 |
|
|
367 |
public class MDBInsertColumnBuilderBase extends InsertColumnBuilderBase { |
|
368 |
@Override |
|
369 |
public String toString(Formatter<ExpressionBuilder.Value> formatter) { |
|
370 |
if( formatter!=null && formatter.canApply(this) ) { |
|
371 |
return formatter.format(this); |
|
372 |
} |
|
373 |
return this.value.toString(formatter); |
|
374 |
} |
|
375 |
} |
|
376 |
|
|
377 |
public class MDBInsertBuilderBase extends InsertBuilderBase { |
|
378 |
@Override |
|
379 |
public String toString(Formatter formatter) { |
|
380 |
return super.toString(formatter); |
|
381 |
} |
|
382 |
} |
|
383 |
|
|
356 | 384 |
public class MDBSelectBuilderBase extends SelectBuilderBase { |
357 | 385 |
|
358 | 386 |
@Override |
... | ... | |
544 | 572 |
} |
545 | 573 |
|
546 | 574 |
@Override |
547 |
protected AlterTableBuilder createAlterTableBuilder() { |
|
575 |
public InsertColumnBuilderBase createInsertColumnBuilder() { |
|
576 |
return new MDBInsertColumnBuilderBase(); |
|
577 |
} |
|
578 |
|
|
579 |
@Override |
|
580 |
public InsertBuilderBase createInsertBuilder() { |
|
581 |
return new MDBInsertBuilderBase(); |
|
582 |
} |
|
583 |
|
|
584 |
@Override |
|
585 |
public AlterTableBuilder createAlterTableBuilder() { |
|
548 | 586 |
return new MDBAlterTableBuilderBase(); |
549 | 587 |
} |
550 | 588 |
|
... | ... | |
567 | 605 |
protected CreateIndexBuilder createCreateIndexBuilder() { |
568 | 606 |
return new MDBCreateIndexBuilder(); |
569 | 607 |
} |
608 |
|
|
609 |
@Override |
|
610 |
public String as_identifier(String id) { |
|
611 |
if (id.startsWith("[")) { |
|
612 |
return id; |
|
613 |
} |
|
614 |
return "["+id+"]"; |
|
615 |
} |
|
616 |
|
|
617 |
@Override |
|
618 |
public Disposable setStatementParameters( |
|
619 |
PreparedStatement st, |
|
620 |
List values, |
|
621 |
GeometryExpressionBuilderHelper.GeometrySupportType geometrySupportType) throws SQLException { |
|
570 | 622 |
|
623 |
if (values == null) { |
|
624 |
return new Disposable() { |
|
625 |
@Override |
|
626 |
public void dispose() { |
|
627 |
} |
|
628 |
}; |
|
629 |
} |
|
630 |
if (true || LOGGER.isDebugEnabled()) { |
|
631 |
StringBuilder debug = new StringBuilder(); |
|
632 |
debug.append("st.set("); |
|
633 |
try { |
|
634 |
byte[] bytes; |
|
635 |
int columnIndex = 1; |
|
636 |
for (Object value : values) { |
|
637 |
if (value instanceof Geometry) { |
|
638 |
switch (geometrySupportType) { |
|
639 |
case WKT: |
|
640 |
value = ((Geometry) value).convertToWKT(); |
|
641 |
debug.append("/*"); |
|
642 |
debug.append(columnIndex); |
|
643 |
debug.append("*/ "); |
|
644 |
debug.append(as_string(value)); |
|
645 |
debug.append(", "); |
|
646 |
break; |
|
647 |
case NATIVE: |
|
648 |
case WKB: |
|
649 |
bytes = ((Geometry) value).convertToWKB(); |
|
650 |
debug.append("/*"); |
|
651 |
debug.append(columnIndex); |
|
652 |
debug.append("*/ "); |
|
653 |
debug.append(as_string(bytes)); |
|
654 |
debug.append(", "); |
|
655 |
break; |
|
656 |
case EWKB: |
|
657 |
bytes = ((Geometry) value).convertToEWKB(); |
|
658 |
debug.append("/*"); |
|
659 |
debug.append(columnIndex); |
|
660 |
debug.append("*/ "); |
|
661 |
debug.append(as_string(bytes)); |
|
662 |
debug.append(", "); |
|
663 |
break; |
|
664 |
} |
|
665 |
} else { |
|
666 |
debug.append("/*"); |
|
667 |
debug.append(columnIndex); |
|
668 |
debug.append("*/ "); |
|
669 |
if (value instanceof String) { |
|
670 |
debug.append(as_string(value)); |
|
671 |
} else if (value instanceof Boolean) { |
|
672 |
debug.append(((Boolean) value) ? constant_true : constant_false); |
|
673 |
} else { |
|
674 |
debug.append(value); |
|
675 |
} |
|
676 |
debug.append(", "); |
|
677 |
} |
|
678 |
columnIndex++; |
|
679 |
} |
|
680 |
debug.append(")"); |
|
681 |
LOGGER.debug(debug.toString()); |
|
682 |
} catch (Exception ex) { |
|
683 |
} |
|
684 |
} |
|
685 |
byte[] bytes; |
|
686 |
int columnIndex = 1; |
|
687 |
Object theValue; |
|
688 |
try { |
|
689 |
for (Object value : values) { |
|
690 |
theValue = value; |
|
691 |
|
|
692 |
if (value instanceof Geometry) { |
|
693 |
switch (geometrySupportType) { |
|
694 |
case WKT: |
|
695 |
value = ((Geometry) value).convertToWKT(); |
|
696 |
st.setObject(columnIndex, value); |
|
697 |
break; |
|
698 |
case NATIVE: |
|
699 |
case WKB: |
|
700 |
bytes = ((Geometry) value).convertToWKB(); |
|
701 |
st.setBytes(columnIndex, bytes); |
|
702 |
break; |
|
703 |
case EWKB: |
|
704 |
bytes = ((Geometry) value).convertToEWKB(); |
|
705 |
st.setBytes(columnIndex, bytes); |
|
706 |
break; |
|
707 |
} |
|
708 |
} else if (value instanceof Date) { |
|
709 |
// Access solo soporta timestamp |
|
710 |
value = new Timestamp(((Date) value).getTime()); |
|
711 |
st.setObject(columnIndex, value); |
|
712 |
} else { |
|
713 |
if (value == null) { |
|
714 |
st.setNull(columnIndex, java.sql.Types.BIT); |
|
715 |
} else { |
|
716 |
st.setObject(columnIndex, value); |
|
717 |
} |
|
718 |
} |
|
719 |
columnIndex++; |
|
720 |
} |
|
721 |
return new Disposable() { |
|
722 |
@Override |
|
723 |
public void dispose() { |
|
724 |
} |
|
725 |
}; |
|
726 |
} catch (Exception ex) { |
|
727 |
throw new SQLException("Can't set values for the prepared statement.", ex); |
|
728 |
} |
|
729 |
} |
|
730 |
|
|
731 |
@Override |
|
732 |
public List<Object> getParameters(FeatureProvider feature) { |
|
733 |
try { |
|
734 |
FeatureType type = feature.getType(); |
|
735 |
List<Object> values = new ArrayList<>(); |
|
736 |
Object value; |
|
737 |
for (Parameter parameter : this.parameters()) { |
|
738 |
if (parameter.is_constant()) { |
|
739 |
value = parameter.value(); |
|
740 |
values.add(value); |
|
741 |
} else { |
|
742 |
String name = parameter.name(); |
|
743 |
value = feature.get(name); |
|
744 |
FeatureAttributeDescriptor attrDesc = type.getAttributeDescriptor(name); |
|
745 |
switch (attrDesc.getType()) { |
|
746 |
case org.gvsig.fmap.dal.DataTypes.BOOLEAN: |
|
747 |
if (value == null) { |
|
748 |
value = false; |
|
749 |
} |
|
750 |
values.add(value); |
|
751 |
break; |
|
752 |
case org.gvsig.fmap.dal.DataTypes.DATE: |
|
753 |
if (value == null) { |
|
754 |
values.add(null); |
|
755 |
} else { |
|
756 |
values.add(new java.sql.Date(((Date) value).getTime())); |
|
757 |
} |
|
758 |
break; |
|
759 |
case org.gvsig.fmap.dal.DataTypes.GEOMETRY: |
|
760 |
Geometry geom = this.forceGeometryType( |
|
761 |
attrDesc.getGeomType(), |
|
762 |
(Geometry) value |
|
763 |
); |
|
764 |
values.add(geom); |
|
765 |
break; |
|
766 |
default: |
|
767 |
values.add(value); |
|
768 |
break; |
|
769 |
} |
|
770 |
} |
|
771 |
} |
|
772 |
return values; |
|
773 |
} catch (Exception ex) { |
|
774 |
String f = "unknow"; |
|
775 |
try { |
|
776 |
f = feature.toString(); |
|
777 |
} catch (Exception ex2) { |
|
778 |
// Do nothing |
|
779 |
} |
|
780 |
throw new RuntimeException("Can't get parameters to prepared statement from the feature (" + f + ")", ex); |
|
781 |
} |
|
782 |
} |
|
783 |
|
|
784 |
|
|
785 |
|
|
786 |
|
|
787 |
|
|
788 |
|
|
571 | 789 |
} |
Also available in: Unified diff