View Javadoc

1   /*
2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector;
18  
19  import java.io.Serializable;
20  import java.sql.Connection;
21  import java.sql.PreparedStatement;
22  import java.sql.ResultSet;
23  import java.sql.SQLException;
24  import java.sql.Timestamp;
25  import java.sql.Types;
26  import java.util.ArrayList;
27  import java.util.List;
28  
29  import javax.sql.DataSource;
30  
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  /**
35   * Represents as persistent, database-backed, store of identifiers.
36   * 
37   * The DDL for the database is
38   * <tt>CREATE TABLE shibpid {localEntity VARCHAR NOT NULL, peerEntity VARCHAR NOT NULL, principalName VARCHAR NOT NULL, localId VARCHAR NOT NULL, persistentId VARCHAR NOT NULL, peerProvidedId VARCHAR, creationDate TIMESTAMP NOT NULL, deactivationDate TIMESTAMP}</tt>.
39   */
40  public class StoredIDStore {
41  
42      /** Class logger. */
43      private final Logger log = LoggerFactory.getLogger(StoredIDStore.class);
44  
45      /** JDBC data source for retrieving connections. */
46      private DataSource dataSource;
47  
48      /** Name of the database table. */
49      private final String table = "shibpid";
50  
51      /** Name of the local entity ID column. */
52      private final String localEntityColumn = "localEntity";
53  
54      /** Name of the peer entity ID name column. */
55      private final String peerEntityColumn = "peerEntity";
56  
57      /** Name of the principal name column. */
58      private final String principalNameColumn = "principalName";
59  
60      /** Name of the local ID column. */
61      private final String localIdColumn = "localId";
62  
63      /** Name of the persistent ID column. */
64      private final String persistentIdColumn = "persistentId";
65  
66      /** ID, provided by peer, associated with the persistent ID. */
67      private final String peerProvidedIdColumn = "peerProvidedId";
68  
69      /** Name of the creation time column. */
70      private final String createTimeColumn = "creationDate";
71  
72      /** Name of the deactivation time column. */
73      private final String deactivationTimeColumn = "deactivationDate";
74  
75      /** Partial select query for ID entries. */
76      private final String idEntrySelectSQL = "SELECT * FROM " + table + " WHERE ";
77  
78      /** SQL used to deactivate an ID. */
79      private final String deactivateIdSQL = "UPDATE " + table + " SET " + deactivationTimeColumn + "= ? WHERE "
80              + persistentIdColumn + "= ?";
81  
82      /**
83       * Constructor.
84       * 
85       * @param source datasource used to communicate with the database
86       */
87      public StoredIDStore(DataSource source) {
88          dataSource = source;
89      }
90  
91      /**
92       * Gets the number of persistent ID entries for a (principal, peer, local) tuple.
93       * 
94       * @param localEntity entity ID of the ID issuer
95       * @param peerEntity entity ID of the peer the ID is for
96       * @param localId local ID part of the persistent ID
97       * 
98       * @return the number of identifiers
99       * 
100      * @throws SQLException thrown if there is a problem communication with the database
101      */
102     public int getNumberOfPersistentIdEntries(String localEntity, String peerEntity, String localId)
103             throws SQLException {
104         StringBuilder sqlBuilder = new StringBuilder();
105         sqlBuilder.append("SELECT");
106         sqlBuilder.append(" count(").append(persistentIdColumn).append(")");
107         sqlBuilder.append(" FROM ").append(table).append(" WHERE ");
108         sqlBuilder.append(localEntityColumn).append(" = ?");
109         sqlBuilder.append(" AND ");
110         sqlBuilder.append(peerEntityColumn).append(" = ?");
111         sqlBuilder.append(" AND ");
112         sqlBuilder.append(localIdColumn).append(" = ?");
113 
114         String sql = sqlBuilder.toString();
115         Connection dbConn = dataSource.getConnection();
116         try {
117             log.debug("Selecting number of persistent ID entries based on prepared sql statement: {}", sql);
118             PreparedStatement statement = dbConn.prepareStatement(sql);
119 
120             log.debug("Setting prepared statement parameter {}: {}", 1, localEntity);
121             statement.setString(1, localEntity);
122             log.debug("Setting prepared statement parameter {}: {}", 2, peerEntity);
123             statement.setString(2, peerEntity);
124             log.debug("Setting prepared statement parameter {}: {}", 3, localId);
125             statement.setString(3, localId);
126 
127             ResultSet rs = statement.executeQuery();
128             rs.next();
129             return rs.getInt(1);
130         } finally {
131             try {
132                 if (dbConn != null && !dbConn.isClosed()) {
133                     dbConn.close();
134                 }
135             } catch (SQLException e) {
136                 log.error("Error closing database connection", e);
137             }
138         }
139     }
140 
141     /**
142      * Gets all the persistent ID entries for a (principal, peer, local) tuple.
143      * 
144      * @param localId local ID part of the persistent ID
145      * @param peerEntity entity ID of the peer the ID is for
146      * @param localEntity entity ID of the ID issuer
147      * 
148      * @return the active identifier
149      * 
150      * @throws SQLException thrown if there is a problem communication with the database
151      */
152     public List<PersistentIdEntry> getPersistentIdEntries(String localEntity, String peerEntity, String localId)
153             throws SQLException {
154         StringBuilder sqlBuilder = new StringBuilder(idEntrySelectSQL);
155         sqlBuilder.append(localEntityColumn).append(" = ?");
156         sqlBuilder.append(" AND ").append(peerEntityColumn).append(" = ?");
157         sqlBuilder.append(" AND ").append(localIdColumn).append(" = ?");
158         String sql = sqlBuilder.toString();
159 
160         log.debug("Selecting all persistent ID entries based on prepared sql statement: {}", sql);
161 
162         Connection dbConn = dataSource.getConnection();
163         try {
164             PreparedStatement statement = dbConn.prepareStatement(sql);
165 
166             log.debug("Setting prepared statement parameter {}: {}", 1, localEntity);
167             statement.setString(1, localEntity);
168             log.debug("Setting prepared statement parameter {}: {}", 2, peerEntity);
169             statement.setString(2, peerEntity);
170             log.debug("Setting prepared statement parameter {}: {}", 3, localId);
171             statement.setString(3, localId);
172 
173             return getIdentifierEntries(statement);
174         } finally {
175             try {
176                 if (dbConn != null && !dbConn.isClosed()) {
177                     dbConn.close();
178                 }
179             } catch (SQLException e) {
180                 log.error("Error closing database connection", e);
181             }
182         }
183     }
184 
185     /**
186      * Gets the persistent ID entry for the given ID.
187      * 
188      * @param persistentId the persistent ID
189      * 
190      * @return the ID entry for the given ID
191      * 
192      * @throws SQLException thrown if there is a problem communication with the database
193      */
194     public PersistentIdEntry getActivePersistentIdEntry(String persistentId) throws SQLException {
195         return getPersistentIdEntry(persistentId, true);
196     }
197 
198     /**
199      * Gets the persistent ID entry for the given ID.
200      * 
201      * @param persistentId the persistent ID
202      * @param onlyActiveId true if only an active ID should be returned, false if a deactivated ID may be returned
203      * 
204      * @return the ID entry for the given ID
205      * 
206      * @throws SQLException thrown if there is a problem communication with the database
207      */
208     public PersistentIdEntry getPersistentIdEntry(String persistentId, boolean onlyActiveId) throws SQLException {
209         StringBuilder sqlBuilder = new StringBuilder(idEntrySelectSQL);
210         sqlBuilder.append(persistentIdColumn).append(" = ?");
211         if (onlyActiveId) {
212             sqlBuilder.append(" AND ").append(deactivationTimeColumn).append(" IS NULL");
213         }
214         String sql = sqlBuilder.toString();
215 
216         log.debug("Selecting persistent ID entry based on prepared sql statement: {}", sql);
217 
218         Connection dbConn = dataSource.getConnection();
219         try {
220             PreparedStatement statement = dbConn.prepareStatement(sql);
221 
222             log.debug("Setting prepared statement parameter {}: {}", 1, persistentId);
223             statement.setString(1, persistentId);
224 
225             List<PersistentIdEntry> entries = getIdentifierEntries(statement);
226 
227             if (entries == null || entries.size() == 0) {
228                 return null;
229             }
230 
231             if (entries.size() > 1) {
232                 log.warn("More than one identifier found, only the first will be used");
233             }
234 
235             return entries.get(0);
236         } finally {
237             try {
238                 if (dbConn != null && !dbConn.isClosed()) {
239                     dbConn.close();
240                 }
241             } catch (SQLException e) {
242                 log.error("Error closing database connection", e);
243             }
244         }
245     }
246 
247     /**
248      * Gets the currently active identifier entry for a (principal, peer, local) tuple.
249      * 
250      * @param localId local ID part of the persistent ID
251      * @param peerEntity entity ID of the peer the ID is for
252      * @param localEntity entity ID of the ID issuer
253      * 
254      * @return the active identifier
255      * 
256      * @throws SQLException thrown if there is a problem communication with the database
257      */
258     public PersistentIdEntry getActivePersistentIdEntry(String localEntity, String peerEntity, String localId)
259             throws SQLException {
260         StringBuilder sqlBuilder = new StringBuilder(idEntrySelectSQL);
261         sqlBuilder.append(localEntityColumn).append(" = ?");
262         sqlBuilder.append(" AND ").append(peerEntityColumn).append(" = ?");
263         sqlBuilder.append(" AND ").append(localIdColumn).append(" = ?");
264         sqlBuilder.append(" AND ").append(deactivationTimeColumn).append(" IS NULL");
265         String sql = sqlBuilder.toString();
266 
267         log.debug("Selecting active persistent ID entry based on prepared sql statement: {}", sql);
268         Connection dbConn = dataSource.getConnection();
269         try {
270             PreparedStatement statement = dbConn.prepareStatement(sql);
271 
272             log.debug("Setting prepared statement parameter {}: {}", 1, localEntity);
273             statement.setString(1, localEntity);
274             log.debug("Setting prepared statement parameter {}: {}", 2, peerEntity);
275             statement.setString(2, peerEntity);
276             log.debug("Setting prepared statement parameter {}: {}", 3, localId);
277             statement.setString(3, localId);
278 
279             log.debug("Getting active persistent Id entries.");
280             List<PersistentIdEntry> entries = getIdentifierEntries(statement);
281 
282             if (entries == null || entries.size() == 0) {
283                 return null;
284             }
285 
286             if (entries.size() > 1) {
287                 log.warn("More than one active identifier, only the first will be used");
288             }
289 
290             return entries.get(0);
291         } finally {
292             try {
293                 if (dbConn != null && !dbConn.isClosed()) {
294                     dbConn.close();
295                 }
296             } catch (SQLException e) {
297                 log.error("Error closing database connection", e);
298             }
299         }
300     }
301 
302     /**
303      * Gets the list of deactivated IDs for a given (principal, peer, local) tuple.
304      * 
305      * @param localId local component of the Id
306      * @param peerEntity entity ID of the peer the ID is for
307      * @param localEntity entity ID of the ID issuer
308      * 
309      * @return list of deactivated identifiers
310      * 
311      * @throws SQLException thrown if there is a problem communication with the database
312      */
313     public List<PersistentIdEntry> getDeactivatedPersistentIdEntries(String localEntity, String peerEntity,
314             String localId) throws SQLException {
315         StringBuilder sqlBuilder = new StringBuilder(idEntrySelectSQL);
316         sqlBuilder.append(localEntityColumn).append(" = ?");
317         sqlBuilder.append(" AND ").append(peerEntityColumn).append(" = ?");
318         sqlBuilder.append(" AND ").append(localIdColumn).append(" = ?");
319         sqlBuilder.append(" AND ").append(deactivationTimeColumn).append(" IS NOT NULL");
320         String sql = sqlBuilder.toString();
321 
322         log.debug("Selecting deactivated persistent ID entries based on prepared sql statement: {}", sql);
323         Connection dbConn = dataSource.getConnection();
324         try {
325             PreparedStatement statement = dbConn.prepareStatement(sql);
326 
327             log.debug("Setting prepared statement parameter {}: {}", 1, localEntity);
328             statement.setString(1, localEntity);
329             log.debug("Setting prepared statement parameter {}: {}", 2, peerEntity);
330             statement.setString(2, peerEntity);
331             log.debug("Setting prepared statement parameter {}: {}", 3, localId);
332             statement.setString(3, localId);
333 
334             log.debug("Getting deactivated persistent Id entries");
335             List<PersistentIdEntry> entries = getIdentifierEntries(statement);
336 
337             if (entries == null || entries.size() == 0) {
338                 return null;
339             }
340 
341             return entries;
342         } finally {
343             try {
344                 if (dbConn != null && !dbConn.isClosed()) {
345                     dbConn.close();
346                 }
347             } catch (SQLException e) {
348                 log.error("Error closing database connection", e);
349             }
350         }
351     }
352 
353     /**
354      * Stores a persistent ID entry into the database.
355      * 
356      * @param entry entry to persist
357      * 
358      * @throws SQLException thrown is there is a problem writing to the database
359      */
360     public void storePersistentIdEntry(PersistentIdEntry entry) throws SQLException {
361 
362         StringBuilder sqlBuilder = new StringBuilder("INSERT INTO ");
363         sqlBuilder.append(table).append(" (");
364         sqlBuilder.append(localEntityColumn).append(", ");
365         sqlBuilder.append(peerEntityColumn).append(", ");
366         sqlBuilder.append(principalNameColumn).append(", ");
367         sqlBuilder.append(localIdColumn).append(", ");
368         sqlBuilder.append(persistentIdColumn).append(", ");
369         sqlBuilder.append(peerProvidedIdColumn).append(", ");
370         sqlBuilder.append(createTimeColumn);
371         sqlBuilder.append(") VALUES (?, ?, ?, ?, ?, ?, ?)");
372 
373         String sql = sqlBuilder.toString();
374 
375         Connection dbConn = dataSource.getConnection();
376         try {
377             log.debug("Storing persistent ID entry based on prepared sql statement: {}", sql);
378             PreparedStatement statement = dbConn.prepareStatement(sql);
379 
380             log.debug("Setting prepared statement parameter {}: {}", 1, entry.getLocalEntityId());
381             statement.setString(1, entry.getLocalEntityId());
382             log.debug("Setting prepared statement parameter {}: {}", 2, entry.getPeerEntityId());
383             statement.setString(2, entry.getPeerEntityId());
384             log.debug("Setting prepared statement parameter {}: {}", 3, entry.getPrincipalName());
385             statement.setString(3, entry.getPrincipalName());
386             log.debug("Setting prepared statement parameter {}: {}", 4, entry.getLocalId());
387             statement.setString(4, entry.getLocalId());
388             log.debug("Setting prepared statement parameter {}: {}", 5, entry.getPersistentId());
389             statement.setString(5, entry.getPersistentId());
390 
391             if (entry.getPeerProvidedId() == null) {
392                 log.debug("Setting prepared statement parameter {}: {}", 6, Types.NULL);
393                 statement.setNull(6, Types.NULL);
394             } else {
395                 log.debug("Setting prepared statement parameter {}: {}", 6, entry.getPeerProvidedId());
396                 statement.setString(6, entry.getPeerProvidedId());
397             }
398             Timestamp timestamp = new Timestamp(System.currentTimeMillis());
399             log.debug("Setting prepared statement parameter {}: {}", 7, timestamp.toString());
400             statement.setTimestamp(7, timestamp);
401 
402             statement.executeUpdate();
403         } finally {
404             try {
405                 if (dbConn != null && !dbConn.isClosed()) {
406                     dbConn.close();
407                 }
408             } catch (SQLException e) {
409                 log.error("Error closing database connection", e);
410             }
411         }
412     }
413 
414     /**
415      * Deactivates a given persistent ID.
416      * 
417      * @param persistentId ID to deactivate
418      * @param deactivation deactivation time, if null the current time is used
419      * 
420      * @throws SQLException thrown if there is a problem communication with the database
421      */
422     public void deactivatePersistentId(String persistentId, Timestamp deactivation) throws SQLException {
423         Timestamp deactivationTime = deactivation;
424         if (deactivationTime == null) {
425             deactivationTime = new Timestamp(System.currentTimeMillis());
426         }
427 
428         Connection dbConn = dataSource.getConnection();
429         try {
430             log.debug("Deactivating persistent id {} as of {}", persistentId, deactivationTime.toString());
431             PreparedStatement statement = dbConn.prepareStatement(deactivateIdSQL);
432             statement.setTimestamp(1, deactivationTime);
433             statement.setString(2, persistentId);
434             statement.executeUpdate();
435         } finally {
436             try {
437                 if (dbConn != null && !dbConn.isClosed()) {
438                     dbConn.close();
439                 }
440             } catch (SQLException e) {
441                 log.error("Error closing database connection", e);
442             }
443         }
444     }
445 
446     /**
447      * Gets a list of {@link PersistentIdEntry}s based on the given prepared statement.
448      * 
449      * @param statement SQL prepared statement
450      * 
451      * @return resultant list of {@link PersistentIdEntry}s
452      * 
453      * @throws SQLException thrown if there is a problem communicating with the database
454      */
455     protected List<PersistentIdEntry> getIdentifierEntries(PreparedStatement statement) throws SQLException {
456         List<PersistentIdEntry> entries;
457         Connection dbConn = dataSource.getConnection();
458         try {
459             ResultSet rs = statement.executeQuery();
460             entries = buildIdentifierEntries(rs);
461             log.debug("{} persistent ID entries retrieved", entries.size());
462             return entries;
463         } finally {
464             try {
465                 if (dbConn != null && !dbConn.isClosed()) {
466                     dbConn.close();
467                 }
468             } catch (SQLException e) {
469                 log.error("Error closing database connection", e);
470             }
471         }
472     }
473 
474     /**
475      * Builds a list of {@link PersistentIdEntry}s from a result set.
476      * 
477      * @param resultSet the result set
478      * 
479      * @return list of {@link PersistentIdEntry}s
480      * 
481      * @throws SQLException thrown if there is a problem reading the information from the database
482      */
483     protected List<PersistentIdEntry> buildIdentifierEntries(ResultSet resultSet) throws SQLException {
484         ArrayList<PersistentIdEntry> entries = new ArrayList<PersistentIdEntry>();
485 
486         PersistentIdEntry entry;
487         while (resultSet.next()) {
488             entry = new PersistentIdEntry();
489             entry.setLocalEntityId(resultSet.getString(localEntityColumn));
490             entry.setPeerEntityId(resultSet.getString(peerEntityColumn));
491             entry.setPrincipalName(resultSet.getString(principalNameColumn));
492             entry.setPersistentId(resultSet.getString(persistentIdColumn));
493             entry.setLocalId(resultSet.getString(localIdColumn));
494             entry.setPeerProvidedId(resultSet.getString(peerProvidedIdColumn));
495             entry.setCreationTime(resultSet.getTimestamp(createTimeColumn));
496             entry.setDeactivationTime(resultSet.getTimestamp(deactivationTimeColumn));
497             entries.add(entry);
498 
499             log.trace("");
500         }
501 
502         return entries;
503     }
504 
505     /** Data object representing a persistent identifier entry in the database. */
506     public class PersistentIdEntry implements Serializable {
507 
508         /** Serial version UID . */
509         private static final long serialVersionUID = -8711779466442306767L;
510 
511         /** ID of the entity that issued that identifier. */
512         private String localEntityId;
513 
514         /** ID of the entity to which the identifier was issued. */
515         private String peerEntityId;
516 
517         /** Name of the principal represented by the identifier. */
518         private String principalName;
519 
520         /** Local component portion of the persistent ID entry. */
521         private String localId;
522 
523         /** The persistent identifier. */
524         private String persistentId;
525 
526         /** ID, associated with the persistent identifier, provided by the peer. */
527         private String peerProvidedId;
528 
529         /** Time the identifier was created. */
530         private Timestamp creationTime;
531 
532         /** Time the identifier was deactivated. */
533         private Timestamp deactivationTime;
534 
535         /** Constructor. */
536         public PersistentIdEntry() {
537         }
538 
539         /**
540          * Gets the ID of the entity that issued the identifier.
541          * 
542          * @return ID of the entity that issued the identifier
543          */
544         public String getLocalEntityId() {
545             return localEntityId;
546         }
547 
548         /**
549          * Sets the ID of the entity that issued the identifier.
550          * 
551          * @param id ID of the entity that issued the identifier
552          */
553         public void setLocalEntityId(String id) {
554             localEntityId = id;
555         }
556 
557         /**
558          * Gets the ID of the entity to which the identifier was issued.
559          * 
560          * @return ID of the entity to which the identifier was issued
561          */
562         public String getPeerEntityId() {
563             return peerEntityId;
564         }
565 
566         /**
567          * Sets the ID of the entity to which the identifier was issued.
568          * 
569          * @param id ID of the entity to which the identifier was issued
570          */
571         public void setPeerEntityId(String id) {
572             peerEntityId = id;
573         }
574 
575         /**
576          * Gets the name of the principal the identifier represents.
577          * 
578          * @return name of the principal the identifier represents
579          */
580         public String getPrincipalName() {
581             return principalName;
582         }
583 
584         /**
585          * Sets the name of the principal the identifier represents.
586          * 
587          * @param name name of the principal the identifier represents
588          */
589         public void setPrincipalName(String name) {
590             principalName = name;
591         }
592 
593         /**
594          * Gets the local ID component of the persistent identifier.
595          * 
596          * @return local ID component of the persistent identifier
597          */
598         public String getLocalId() {
599             return localId;
600         }
601 
602         /**
603          * Sets the local ID component of the persistent identifier.
604          * 
605          * @param id local ID component of the persistent identifier
606          */
607         public void setLocalId(String id) {
608             localId = id;
609         }
610 
611         /**
612          * Gets the persistent identifier.
613          * 
614          * @return the persistent identifier
615          */
616         public String getPersistentId() {
617             return persistentId;
618         }
619 
620         /**
621          * Set the persistent identifier.
622          * 
623          * @param id the persistent identifier
624          */
625         public void setPersistentId(String id) {
626             persistentId = id;
627         }
628 
629         /**
630          * Gets the ID, provided by the peer, associated with this ID.
631          * 
632          * @return ID, provided by the peer, associated with this ID
633          */
634         public String getPeerProvidedId() {
635             return peerProvidedId;
636         }
637 
638         /**
639          * Sets the ID, provided by the peer, associated with this ID.
640          * 
641          * @param id ID, provided by the peer, associated with this ID
642          */
643         public void setPeerProvidedId(String id) {
644             peerProvidedId = id;
645         }
646 
647         /**
648          * Gets the time the identifier was created.
649          * 
650          * @return time the identifier was created
651          */
652         public Timestamp getCreationTime() {
653             return creationTime;
654         }
655 
656         /**
657          * Sets the time the identifier was created.
658          * 
659          * @param time time the identifier was created
660          */
661         public void setCreationTime(Timestamp time) {
662             creationTime = time;
663         }
664 
665         /**
666          * Gets the time the identifier was deactivated.
667          * 
668          * @return time the identifier was deactivated
669          */
670         public Timestamp getDeactivationTime() {
671             return deactivationTime;
672         }
673 
674         /**
675          * Sets the time the identifier was deactivated.
676          * 
677          * @param time the time the identifier was deactivated
678          */
679         public void setDeactivationTime(Timestamp time) {
680             this.deactivationTime = time;
681         }
682 
683         /** {@inheritDoc} */
684         public String toString() {
685             StringBuilder stringForm = new StringBuilder("PersistentIdEntry{");
686             stringForm.append("persistentId:").append(persistentId).append(", ");
687             stringForm.append("localEntityId:").append(localEntityId).append(", ");
688             stringForm.append("peerEntityId:").append(peerEntityId).append(", ");
689             stringForm.append("localId:").append(localId).append(", ");
690             stringForm.append("principalName:").append(principalName).append(", ");
691             stringForm.append("peerProvidedId:").append(peerProvidedId).append(", ");
692             stringForm.append("creationTime:").append(creationTime).append(", ");
693             stringForm.append("deactivationTime:").append(deactivationTime).append(", ");
694             stringForm.append("}");
695             return stringForm.toString();
696         }
697     }
698 }