File D:\maven-qcr-plugin-testsunitaires\qcr4java\h2-1.3.170-sources\target\site\qc\lecode\server\pg\PgServerThread.java.html

1 /*
2 * Copyright 2004-2011 H2 Group. Multiple-Licensed under the H2 License,
3 * Version 1.0, and under the Eclipse Public License, Version 1.0
4 * (http://h2database.com/html/license.html).
5 * Initial Developer: H2 Group
6 */
7 package org.h2.server.pg;
8
9 import java.io.ByteArrayInputStream;
10 import java.io.ByteArrayOutputStream;
11 import java.io.DataInputStream;
12 import java.io.DataOutputStream;
13 import java.io.EOFException;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.InputStreamReader;
17 import java.io.OutputStream;
18 import java.io.Reader;
19 import java.io.StringReader;
20 import java.net.Socket;
21 import java.sql.Connection;
22 import java.sql.ParameterMetaData;
23 import java.sql.PreparedStatement;
24 import java.sql.ResultSet;
25 import java.sql.ResultSetMetaData;
26 import java.sql.SQLException;
27 import java.sql.Statement;
28 import java.util.HashMap;
29 import java.util.HashSet;
30 import java.util.Properties;
31 import org.h2.command.CommandInterface;
32 import org.h2.constant.SysProperties;
33 import org.h2.engine.ConnectionInfo;
34 import org.h2.jdbc.JdbcConnection;
35 import org.h2.jdbc.JdbcPreparedStatement;
36 import org.h2.jdbc.JdbcStatement;
37 import org.h2.message.DbException;
38 import org.h2.util.IOUtils;
39 import org.h2.util.JdbcUtils;
40 import org.h2.util.ScriptReader;
41 import org.h2.util.StringUtils;
42 import org.h2.util.Utils;
43 import org.h2.value.CaseInsensitiveMap;
44
45 /**
46 * One server thread is opened for each client.
47 */
48 public class PgServerThread implements Runnable {
49 private final PgServer server;
50 private Socket socket;
51 private Connection conn;
52 private boolean stop;
53 private DataInputStream dataInRaw;
54 private DataInputStream dataIn;
55 private OutputStream out;
56 private int messageType;
57 private ByteArrayOutputStream outBuffer;
58 private DataOutputStream dataOut;
59 private Thread thread;
60 private boolean initDone;
61 private String userName;
62 private String databaseName;
63 private int processId;
64 private String clientEncoding = SysProperties.PG_DEFAULT_CLIENT_ENCODING;
65 private String dateStyle = "ISO";
66 private final HashMap<String, Prepared> prepared = new CaseInsensitiveMap<Prepared>();
67 private final HashMap<String, Portal> portals = new CaseInsensitiveMap<Portal>();
68
69 PgServerThread(Socket socket, PgServer server) {
70 this.server = server;
71 this.socket = socket;
72 }
73
74 public void run() {
75 try {
76 server.trace("Connect");
77 InputStream ins = socket.getInputStream();
78 out = socket.getOutputStream();
79 dataInRaw = new DataInputStream(ins);
80 while (!stop) {
81 process();
82 out.flush();
83 }
84 } catch (EOFException e) {
85 // more or less normal disconnect
86 } catch (Exception e) {
87 server.traceError(e);
88 } finally {
89 server.trace("Disconnect");
90 close();
91 }
92 }
93
94 private String readString() throws IOException {
95 ByteArrayOutputStream buff = new ByteArrayOutputStream();
96 while (true) {
97 int x = dataIn.read();
98 if (x <= 0) {
99 break;
100 }
101 buff.write(x);
102 }
103 return new String(buff.toByteArray(), getEncoding());
104 }
105
106 private int readInt() throws IOException {
107 return dataIn.readInt();
108 }
109
110 private int readShort() throws IOException {
111 return dataIn.readShort();
112 }
113
114 private byte readByte() throws IOException {
115 return dataIn.readByte();
116 }
117
118 private void readFully(byte[] buff) throws IOException {
119 dataIn.readFully(buff);
120 }
121
122 private void process() throws IOException {
123 int x;
124 if (initDone) {
125 x = dataInRaw.read();
126 if (x < 0) {
127 stop = true;
128 return;
129 }
130 } else {
131 x = 0;
132 }
133 int len = dataInRaw.readInt();
134 len -= 4;
135 byte[] data = Utils.newBytes(len);
136 dataInRaw.readFully(data, 0, len);
137 dataIn = new DataInputStream(new ByteArrayInputStream(data, 0, len));
138 switchBlock: switch (x) {
139 case 0:
140 server.trace("Init");
141 int version = readInt();
142 if (version == 80877102) {
143 server.trace("CancelRequest (not supported)");
144 server.trace(" pid: " + readInt());
145 server.trace(" key: " + readInt());
146 } else if (version == 80877103) {
147 server.trace("SSLRequest");
148 out.write('N');
149 } else {
150 server.trace("StartupMessage");
151 server.trace(" version " + version + " (" + (version >> 16) + "." + (version & 0xff) + ")");
152 while (true) {
153 String param = readString();
154 if (param.length() == 0) {
155 break;
156 }
157 String value = readString();
158 if ("user".equals(param)) {
159 this.userName = value;
160 } else if ("database".equals(param)) {
161 this.databaseName = value;
162 } else if ("client_encoding".equals(param)) {
163 // UTF8
164 clientEncoding = value;
165 } else if ("DateStyle".equals(param)) {
166 dateStyle = value;
167 }
168 // extra_float_digits 2
169 // geqo on (Genetic Query Optimization)
170 server.trace(" param " + param + "=" + value);
171 }
172 sendAuthenticationCleartextPassword();
173 initDone = true;
174 }
175 break;
176 case 'p': {
177 server.trace("PasswordMessage");
178 String password = readString();
179 try {
180 Properties info = new Properties();
181 info.put("MODE", "PostgreSQL");
182 info.put("USER", userName);
183 info.put("PASSWORD", password);
184 String url = "jdbc:h2:" + databaseName;
185 ConnectionInfo ci = new ConnectionInfo(url, info);
186 String baseDir = server.getBaseDir();
187 if (baseDir == null) {
188 baseDir = SysProperties.getBaseDir();
189 }
190 if (baseDir != null) {
191 ci.setBaseDir(baseDir);
192 }
193 if (server.getIfExists()) {
194 ci.setProperty("IFEXISTS", "TRUE");
195 }
196 conn = new JdbcConnection(ci, false);
197 // can not do this because when called inside
198 // DriverManager.getConnection, a deadlock occurs
199 // conn = DriverManager.getConnection(url, userName, password);
200 initDb();
201 sendAuthenticationOk();
202 } catch (Exception e) {
203 e.printStackTrace();
204 stop = true;
205 }
206 break;
207 }
208 case 'P': {
209 server.trace("Parse");
210 Prepared p = new Prepared();
211 p.name = readString();
212 p.sql = getSQL(readString());
213 int count = readShort();
214 p.paramType = new int[count];
215 for (int i = 0; i < count; i++) {
216 int type = readInt();
217 server.checkType(type);
218 p.paramType[i] = type;
219 }
220 try {
221 p.prep = (JdbcPreparedStatement) conn.prepareStatement(p.sql);
222 prepared.put(p.name, p);
223 sendParseComplete();
224 } catch (Exception e) {
225 sendErrorResponse(e);
226 }
227 break;
228 }
229 case 'B': {
230 server.trace("Bind");
231 Portal portal = new Portal();
232 portal.name = readString();
233 String prepName = readString();
234 Prepared prep = prepared.get(prepName);
235 if (prep == null) {
236 sendErrorResponse("Prepared not found");
237 break;
238 }
239 portal.prep = prep;
240 portals.put(portal.name, portal);
241 int formatCodeCount = readShort();
242 int[] formatCodes = new int[formatCodeCount];
243 for (int i = 0; i < formatCodeCount; i++) {
244 formatCodes[i] = readShort();
245 }
246 int paramCount = readShort();
247 for (int i = 0; i < paramCount; i++) {
248 int paramLen = readInt();
249 byte[] d2 = Utils.newBytes(paramLen);
250 readFully(d2);
251 try {
252 setParameter(prep.prep, i, d2, formatCodes);
253 } catch (Exception e) {
254 sendErrorResponse(e);
255 break switchBlock;
256 }
257 }
258 int resultCodeCount = readShort();
259 portal.resultColumnFormat = new int[resultCodeCount];
260 for (int i = 0; i < resultCodeCount; i++) {
261 portal.resultColumnFormat[i] = readShort();
262 }
263 sendBindComplete();
264 break;
265 }
266 case 'C': {
267 char type = (char) readByte();
268 String name = readString();
269 server.trace("Close");
270 if (type == 'S') {
271 Prepared p = prepared.remove(name);
272 if (p != null) {
273 JdbcUtils.closeSilently(p.prep);
274 }
275 } else if (type == 'P') {
276 portals.remove(name);
277 } else {
278 server.trace("expected S or P, got " + type);
279 sendErrorResponse("expected S or P");
280 break;
281 }
282 sendCloseComplete();
283 break;
284 }
285 case 'D': {
286 char type = (char) readByte();
287 String name = readString();
288 server.trace("Describe");
289 if (type == 'S') {
290 Prepared p = prepared.get(name);
291 if (p == null) {
292 sendErrorResponse("Prepared not found: " + name);
293 } else {
294 sendParameterDescription(p);
295 }
296 } else if (type == 'P') {
297 Portal p = portals.get(name);
298 if (p == null) {
299 sendErrorResponse("Portal not found: " + name);
300 } else {
301 PreparedStatement prep = p.prep.prep;
302 try {
303 ResultSetMetaData meta = prep.getMetaData();
304 sendRowDescription(meta);
305 } catch (Exception e) {
306 sendErrorResponse(e);
307 }
308 }
309 } else {
310 server.trace("expected S or P, got " + type);
311 sendErrorResponse("expected S or P");
312 }
313 break;
314 }
315 case 'E': {
316 String name = readString();
317 server.trace("Execute");
318 Portal p = portals.get(name);
319 if (p == null) {
320 sendErrorResponse("Portal not found: " + name);
321 break;
322 }
323 int maxRows = readShort();
324 Prepared prepared = p.prep;
325 JdbcPreparedStatement prep = prepared.prep;
326 server.trace(prepared.sql);
327 try {
328 prep.setMaxRows(maxRows);
329 boolean result = prep.execute();
330 if (result) {
331 try {
332 ResultSet rs = prep.getResultSet();
333 ResultSetMetaData meta = rs.getMetaData();
334 sendRowDescription(meta);
335 while (rs.next()) {
336 sendDataRow(rs);
337 }
338 sendCommandComplete(prep, 0);
339 } catch (Exception e) {
340 sendErrorResponse(e);
341 }
342 } else {
343 sendCommandComplete(prep, prep.getUpdateCount());
344 }
345 } catch (Exception e) {
346 sendErrorResponse(e);
347 }
348 break;
349 }
350 case 'S': {
351 server.trace("Sync");
352 sendReadyForQuery();
353 break;
354 }
355 case 'Q': {
356 server.trace("Query");
357 String query = readString();
358 ScriptReader reader = new ScriptReader(new StringReader(query));
359 while (true) {
360 JdbcStatement stat = null;
361 try {
362 String s = reader.readStatement();
363 if (s == null) {
364 break;
365 }
366 s = getSQL(s);
367 stat = (JdbcStatement) conn.createStatement();
368 boolean result = stat.execute(s);
369 if (result) {
370 ResultSet rs = stat.getResultSet();
371 ResultSetMetaData meta = rs.getMetaData();
372 try {
373 sendRowDescription(meta);
374 while (rs.next()) {
375 sendDataRow(rs);
376 }
377 sendCommandComplete(stat, 0);
378 } catch (Exception e) {
379 sendErrorResponse(e);
380 break;
381 }
382 } else {
383 sendCommandComplete(stat, stat.getUpdateCount());
384 }
385 } catch (SQLException e) {
386 sendErrorResponse(e);
387 break;
388 } finally {
389 JdbcUtils.closeSilently(stat);
390 }
391 }
392 sendReadyForQuery();
393 break;
394 }
395 case 'X': {
396 server.trace("Terminate");
397 close();
398 break;
399 }
400 default:
401 server.trace("Unsupported: " + x + " (" + (char) x + ")");
402 break;
403 }
404 }
405
406 private String getSQL(String s) {
407 String lower = StringUtils.toLowerEnglish(s);
408 if (lower.startsWith("show max_identifier_length")) {
409 s = "CALL 63";
410 } else if (lower.startsWith("set client_encoding to")) {
411 s = "set DATESTYLE ISO";
412 }
413 // s = StringUtils.replaceAll(s, "i.indkey[ia.attnum-1]", "0");
414 if (server.getTrace()) {
415 server.trace(s + ";");
416 }
417 return s;
418 }
419
420 private void sendCommandComplete(JdbcStatement stat, int updateCount) throws IOException {
421 startMessage('C');
422 switch (stat.getLastExecutedCommandType()) {
423 case CommandInterface.INSERT:
424 writeStringPart("INSERT 0 ");
425 writeString(Integer.toString(updateCount));
426 break;
427 case CommandInterface.UPDATE:
428 writeStringPart("UPDATE ");
429 writeString(Integer.toString(updateCount));
430 break;
431 case CommandInterface.DELETE:
432 writeStringPart("DELETE ");
433 writeString(Integer.toString(updateCount));
434 break;
435 case CommandInterface.SELECT:
436 case CommandInterface.CALL:
437 writeString("SELECT");
438 break;
439 case CommandInterface.BEGIN:
440 writeString("BEGIN");
441 break;
442 default:
443 server.trace("check CommandComplete tag for command " + stat);
444 writeStringPart("UPDATE ");
445 writeString(Integer.toString(updateCount));
446 }
447 sendMessage();
448 }
449
450 private void sendDataRow(ResultSet rs) throws Exception {
451 int columns = rs.getMetaData().getColumnCount();
452 String[] values = new String[columns];
453 for (int i = 0; i < columns; i++) {
454 values[i] = rs.getString(i + 1);
455 }
456 startMessage('D');
457 writeShort(columns);
458 for (String s : values) {
459 if (s == null) {
460 writeInt(-1);
461 } else {
462 // TODO write Binary data
463 byte[] d2 = s.getBytes(getEncoding());
464 writeInt(d2.length);
465 write(d2);
466 }
467 }
468 sendMessage();
469 }
470
471 private String getEncoding() {
472 if ("UNICODE".equals(clientEncoding)) {
473 return "UTF-8";
474 }
475 return clientEncoding;
476 }
477
478 private void setParameter(PreparedStatement prep, int i, byte[] d2, int[] formatCodes) throws SQLException {
479 boolean text = (i >= formatCodes.length) || (formatCodes[i] == 0);
480 String s;
481 try {
482 if (text) {
483 s = new String(d2, getEncoding());
484 } else {
485 server.trace("Binary format not supported");
486 s = new String(d2, getEncoding());
487 }
488 } catch (Exception e) {
489 server.traceError(e);
490 s = null;
491 }
492 // if(server.getLog()) {
493 // server.log(" " + i + ": " + s);
494 // }
495 prep.setString(i + 1, s);
496 }
497
498 private void sendErrorResponse(Exception re) throws IOException {
499 SQLException e = DbException.toSQLException(re);
500 server.traceError(e);
501 startMessage('E');
502 write('S');
503 writeString("ERROR");
504 write('C');
505 writeString(e.getSQLState());
506 write('M');
507 writeString(e.getMessage());
508 write('D');
509 writeString(e.toString());
510 write(0);
511 sendMessage();
512 }
513
514 private void sendParameterDescription(Prepared p) throws IOException {
515 try {
516 PreparedStatement prep = p.prep;
517 ParameterMetaData meta = prep.getParameterMetaData();
518 int count = meta.getParameterCount();
519 startMessage('t');
520 writeShort(count);
521 for (int i = 0; i < count; i++) {
522 int type;
523 if (p.paramType != null && p.paramType[i] != 0) {
524 type = p.paramType[i];
525 } else {
526 type = PgServer.PG_TYPE_VARCHAR;
527 }
528 server.checkType(type);
529 writeInt(type);
530 }
531 sendMessage();
532 } catch (Exception e) {
533 sendErrorResponse(e);
534 }
535 }
536
537 private void sendNoData() throws IOException {
538 startMessage('n');
539 sendMessage();
540 }
541
542 private void sendRowDescription(ResultSetMetaData meta) throws Exception {
543 if (meta == null) {
544 sendNoData();
545 } else {
546 int columns = meta.getColumnCount();
547 int[] types = new int[columns];
548 int[] precision = new int[columns];
549 String[] names = new String[columns];
550 for (int i = 0; i < columns; i++) {
551 String name = meta.getColumnName(i + 1);
552 names[i] = name;
553 int type = meta.getColumnType(i + 1);
554 type = PgServer.convertType(type);
555 // the ODBC client needs the column pg_catalog.pg_index
556 // to be of type 'int2vector'
557 // if (name.equalsIgnoreCase("indkey") &&
558 // "pg_index".equalsIgnoreCase(meta.getTableName(i + 1))) {
559 // type = PgServer.PG_TYPE_INT2VECTOR;
560 // }
561 precision[i] = meta.getColumnDisplaySize(i + 1);
562 server.checkType(type);
563 types[i] = type;
564 }
565 startMessage('T');
566 writeShort(columns);
567 for (int i = 0; i < columns; i++) {
568 writeString(StringUtils.toLowerEnglish(names[i]));
569 // object ID
570 writeInt(0);
571 // attribute number of the column
572 writeShort(0);
573 // data type
574 writeInt(types[i]);
575 // pg_type.typlen
576 writeShort(getTypeSize(types[i], precision[i]));
577 // pg_attribute.atttypmod
578 writeInt(-1);
579 // text
580 writeShort(0);
581 }
582 sendMessage();
583 }
584 }
585
586 private static int getTypeSize(int pgType, int precision) {
587 switch (pgType) {
588 case PgServer.PG_TYPE_VARCHAR:
589 return Math.max(255, precision + 10);
590 default:
591 return precision + 4;
592 }
593 }
594
595 private void sendErrorResponse(String message) throws IOException {
596 server.trace("Exception: " + message);
597 startMessage('E');
598 write('S');
599 writeString("ERROR");
600 write('C');
601 // PROTOCOL VIOLATION
602 writeString("08P01");
603 write('M');
604 writeString(message);
605 sendMessage();
606 }
607
608 private void sendParseComplete() throws IOException {
609 startMessage('1');
610 sendMessage();
611 }
612
613 private void sendBindComplete() throws IOException {
614 startMessage('2');
615 sendMessage();
616 }
617
618 private void sendCloseComplete() throws IOException {
619 startMessage('3');
620 sendMessage();
621 }
622
623 private void initDb() throws SQLException {
624 Statement stat = null;
625 ResultSet rs = null;
626 try {
627 synchronized (server) {
628 // better would be: set the database to exclusive mode
629 rs = conn.getMetaData().getTables(null, "PG_CATALOG", "PG_VERSION", null);
630 boolean tableFound = rs.next();
631 stat = conn.createStatement();
632 if (!tableFound) {
633 installPgCatalog(stat);
634 }
635 rs = stat.executeQuery("SELECT * FROM PG_CATALOG.PG_VERSION");
636 if (!rs.next() || rs.getInt(1) < 2) {
637 // installation incomplete, or old version
638 installPgCatalog(stat);
639 } else {
640 // version 2 or newer: check the read version
641 int versionRead = rs.getInt(2);
642 if (versionRead > 2) {
643 throw DbException.throwInternalError("Incompatible PG_VERSION");
644 }
645 }
646 }
647 stat.execute("set search_path = PUBLIC, pg_catalog");
648 HashSet<Integer> typeSet = server.getTypeSet();
649 if (typeSet.size() == 0) {
650 rs = stat.executeQuery("SELECT OID FROM PG_CATALOG.PG_TYPE");
651 while (rs.next()) {
652 typeSet.add(rs.getInt(1));
653 }
654 }
655 } finally {
656 JdbcUtils.closeSilently(stat);
657 JdbcUtils.closeSilently(rs);
658 }
659 }
660
661 private static void installPgCatalog(Statement stat) throws SQLException {
662 Reader r = null;
663 try {
664 r = new InputStreamReader(new ByteArrayInputStream(Utils
665 .getResource("/org/h2/server/pg/pg_catalog.sql")));
666 ScriptReader reader = new ScriptReader(r);
667 while (true) {
668 String sql = reader.readStatement();
669 if (sql == null) {
670 break;
671 }
672 stat.execute(sql);
673 }
674 reader.close();
675 } catch (IOException e) {
676 throw DbException.convertIOException(e, "Can not read pg_catalog resource");
677 } finally {
678 IOUtils.closeSilently(r);
679 }
680 }
681
682 /**
683 * Close this connection.
684 */
685 void close() {
686 try {
687 stop = true;
688 JdbcUtils.closeSilently(conn);
689 if (socket != null) {
690 socket.close();
691 }
692 server.trace("Close");
693 } catch (Exception e) {
694 server.traceError(e);
695 }
696 conn = null;
697 socket = null;
698 server.remove(this);
699 }
700
701 private void sendAuthenticationCleartextPassword() throws IOException {
702 startMessage('R');
703 writeInt(3);
704 sendMessage();
705 }
706
707 private void sendAuthenticationOk() throws IOException {
708 startMessage('R');
709 writeInt(0);
710 sendMessage();
711 sendParameterStatus("client_encoding", clientEncoding);
712 sendParameterStatus("DateStyle", dateStyle);
713 sendParameterStatus("integer_datetimes", "off");
714 sendParameterStatus("is_superuser", "off");
715 sendParameterStatus("server_encoding", "SQL_ASCII");
716 sendParameterStatus("server_version", "8.1.4");
717 sendParameterStatus("session_authorization", userName);
718 sendParameterStatus("standard_conforming_strings", "off");
719 // TODO PostgreSQL TimeZone
720 sendParameterStatus("TimeZone", "CET");
721 sendBackendKeyData();
722 sendReadyForQuery();
723 }
724
725 private void sendReadyForQuery() throws IOException {
726 startMessage('Z');
727 char c;
728 try {
729 if (conn.getAutoCommit()) {
730 // idle
731 c = 'I';
732 } else {
733 // in a transaction block
734 c = 'T';
735 }
736 } catch (SQLException e) {
737 // failed transaction block
738 c = 'E';
739 }
740 write((byte) c);
741 sendMessage();
742 }
743
744 private void sendBackendKeyData() throws IOException {
745 startMessage('K');
746 writeInt(processId);
747 writeInt(processId);
748 sendMessage();
749 }
750
751 private void writeString(String s) throws IOException {
752 writeStringPart(s);
753 write(0);
754 }
755
756 private void writeStringPart(String s) throws IOException {
757 write(s.getBytes(getEncoding()));
758 }
759
760 private void writeInt(int i) throws IOException {
761 dataOut.writeInt(i);
762 }
763
764 private void writeShort(int i) throws IOException {
765 dataOut.writeShort(i);
766 }
767
768 private void write(byte[] data) throws IOException {
769 dataOut.write(data);
770 }
771
772 private void write(int b) throws IOException {
773 dataOut.write(b);
774 }
775
776 private void startMessage(int newMessageType) {
777 this.messageType = newMessageType;
778 outBuffer = new ByteArrayOutputStream();
779 dataOut = new DataOutputStream(outBuffer);
780 }
781
782 private void sendMessage() throws IOException {
783 dataOut.flush();
784 byte[] buff = outBuffer.toByteArray();
785 int len = buff.length;
786 dataOut = new DataOutputStream(out);
787 dataOut.write(messageType);
788 dataOut.writeInt(len + 4);
789 dataOut.write(buff);
790 dataOut.flush();
791 }
792
793 private void sendParameterStatus(String param, String value) throws IOException {
794 startMessage('S');
795 writeString(param);
796 writeString(value);
797 sendMessage();
798 }
799
800 void setThread(Thread thread) {
801 this.thread = thread;
802 }
803
804 Thread getThread() {
805 return thread;
806 }
807
808 void setProcessId(int id) {
809 this.processId = id;
810 }
811
812 /**
813 * Represents a PostgreSQL Prepared object.
814 */
815 static class Prepared {
816
817 /**
818 * The object name.
819 */
820 String name;
821
822 /**
823 * The SQL statement.
824 */
825 String sql;
826
827 /**
828 * The prepared statement.
829 */
830 JdbcPreparedStatement prep;
831
832 /**
833 * The list of parameter types (if set).
834 */
835 int[] paramType;
836 }
837
838 /**
839 * Represents a PostgreSQL Portal object.
840 */
841 static class Portal {
842
843 /**
844 * The portal name.
845 */
846 String name;
847
848 /**
849 * The format used in the result set columns (if set).
850 */
851 int[] resultColumnFormat;
852
853 /**
854 * The prepared object.
855 */
856 Prepared prep;
857 }
858
859 }
860