View Javadoc
1   /**
2    * This Source Code Form is subject to the terms of the Mozilla Public
3    * License, v. 2.0. If a copy of the MPL was not distributed with this
4    * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5    *
6    * If it is not possible or desirable to put the notice in a particular
7    * file, then You may include the notice in a location (such as a LICENSE
8    * file in a relevant directory) where a recipient would be likely to look
9    * for such a notice.
10   *
11   * 
12   */
13  /*  ---------------------------------------------------------------------------
14   *  U.S. Government, Department of the Army
15   *  Army Materiel Command
16   *  Research Development Engineering Command
17   *  Communications Electronics Research Development and Engineering Center
18   *  ---------------------------------------------------------------------------
19   */
20  package org.miloss.fgsms.services.ars.impl;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.StringWriter;
24  import java.sql.Connection;
25  import java.sql.PreparedStatement;
26  import java.sql.ResultSet;
27  import java.sql.SQLException;
28  import java.util.GregorianCalendar;
29  import java.util.UUID;
30  import javax.annotation.Resource;
31  import javax.jws.WebMethod;
32  import javax.jws.WebParam;
33  import javax.jws.WebResult;
34  import javax.jws.WebService;
35  import javax.xml.bind.JAXBContext;
36  import javax.xml.bind.JAXBElement;
37  import javax.xml.bind.Unmarshaller;
38  import javax.xml.bind.annotation.XmlSeeAlso;
39  import javax.xml.datatype.DatatypeConfigurationException;
40  import javax.xml.datatype.DatatypeFactory;
41  import java.util.Calendar;
42  import javax.xml.stream.XMLInputFactory;
43  import javax.xml.stream.XMLStreamReader;
44  import javax.xml.ws.RequestWrapper;
45  import javax.xml.ws.ResponseWrapper;
46  import javax.xml.ws.WebServiceContext;
47  import org.miloss.fgsms.common.AuditLogger;
48  import org.miloss.fgsms.common.DBSettingsLoader;
49  import org.miloss.fgsms.common.Constants;
50  import org.miloss.fgsms.common.UserIdentityUtil;
51  import org.miloss.fgsms.common.Utility;
52  import org.miloss.fgsms.services.interfaces.automatedreportingservice.*;
53  import org.miloss.fgsms.services.interfaces.automatedreportingservice.AccessDeniedException;
54  import org.miloss.fgsms.services.interfaces.automatedreportingservice.ServiceUnavailableException;
55  import org.miloss.fgsms.services.interfaces.common.GetOperatingStatusRequestMessage;
56  import org.miloss.fgsms.services.interfaces.common.GetOperatingStatusResponseMessage;
57  import org.miloss.fgsms.services.interfaces.common.SecurityWrapper;
58  import org.miloss.fgsms.services.interfaces.faults.ServiceUnavailableFaultCodes;
59  import org.miloss.fgsms.services.interfaces.policyconfiguration.*;
60  import org.miloss.fgsms.services.interfaces.reportingservice.ExportRecordsEnum;
61  import org.apache.log4j.Level;
62  import org.miloss.fgsms.common.Logger;
63  ;
64  import org.miloss.fgsms.common.DBUtils;
65  import org.miloss.fgsms.plugins.reporting.ReportGeneratorPlugin;
66  import org.miloss.fgsms.services.interfaces.reportingservice.ReportTypeContainer;
67  import us.gov.ic.ism.v2.ClassificationType;
68  
69  /**
70   * Reporting Service Port Type This service basically reads and writes to the
71   * database to set/get/fetch/delete reports and report jobs
72   *
73   * @since 6.2
74   */
75  
76  
77  @WebService(name = "automatedReportingService", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService"
78  //, wsdlLocation = "ARSv1.wsdl"
79  )
80  @XmlSeeAlso({
81      org.miloss.fgsms.services.interfaces.policyconfiguration.ObjectFactory.class,
82      us.gov.ic.ism.v2.ObjectFactory.class,
83      org.miloss.fgsms.services.interfaces.reportingservice.ObjectFactory.class,
84      org.miloss.fgsms.services.interfaces.common.ObjectFactory.class,
85      org.miloss.fgsms.services.interfaces.faults.ObjectFactory.class,
86      org.miloss.fgsms.services.interfaces.automatedreportingservice.ObjectFactory.class
87  })
88  public class AutomatedReportingServiceImpl implements AutomatedReportingService, org.miloss.fgsms.services.interfaces.automatedreportingservice.OpStatusService {
89  
90      private static final String name = "fgsms.AutomatedReportingService";
91      private static DatatypeFactory df = null;
92      private static Calendar started = null;
93      final static Logger log = Logger.getLogger(name);
94      private static final String OK = "OK";
95  
96      private static void validatePluginRegistered(String type) throws Exception {
97          Connection configurationDBConnection = Utility.getConfigurationDBConnection();
98          PreparedStatement cmd = null;
99          ResultSet rs = null;
100         try {
101             cmd = configurationDBConnection.prepareStatement("select * from plugins where classname=? and appliesto='REPORTING'");
102             cmd.setString(1, type);
103             ResultSet executeQuery = cmd.executeQuery();
104             if (executeQuery.next()) {
105                 //we are ok
106             } else {
107                 throw new IllegalArgumentException("Plugin '" + type + "' not registered");
108             }
109         } catch (Exception ex) {
110             log.log(Level.WARN, null, ex);
111             throw ex;
112         } finally {
113             DBUtils.safeClose(rs);
114             DBUtils.safeClose(cmd);
115             DBUtils.safeClose(configurationDBConnection);
116         }
117     }
118 
119     public AutomatedReportingServiceImpl() throws DatatypeConfigurationException {
120 
121         Init();
122     }
123 
124     /**
125      * constructor used for unit testing, do not remove
126      *
127      * @param w
128      */
129     public AutomatedReportingServiceImpl(WebServiceContext w) throws DatatypeConfigurationException {
130         ctx = w;
131         Init();
132     }
133     @Resource
134     private WebServiceContext ctx;
135 
136     private synchronized void Init() throws DatatypeConfigurationException {
137         if (df == null) {
138             df = DatatypeFactory.newInstance();
139         }
140         if (started == null) {
141             GregorianCalendar gcal = new GregorianCalendar();
142             gcal.setTimeInMillis(System.currentTimeMillis());
143             started = (gcal);
144         }
145     }
146 
147     /**
148      *     * Gets a list of jobs owned by the current user and a list of recently
149      * generated reports
150      *
151      * @param request
152      * @return returns
153      * org.miloss.fgsms.services.interfaces.automatedreportingservice.GetMyScheduledReportsResponseMsg
154      * @throws AccessDeniedException
155      * @throws ServiceUnavailableException
156      */
157     @WebMethod(operationName = "GetMyScheduledReports", action = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService/GetMyScheduledReports")
158     @WebResult(name = "response", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService")
159     @RequestWrapper(localName = "GetMyScheduledReports", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.GetMyScheduledReports")
160     @ResponseWrapper(localName = "GetMyScheduledReportsResponse", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.GetMyScheduledReportsResponse")
161     public GetMyScheduledReportsResponseMsg getMyScheduledReports(
162             @WebParam(name = "request", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService") GetMyScheduledReportsRequestMsg request)
163             throws AccessDeniedException, ServiceUnavailableException {
164         String currentUser = UserIdentityUtil.getFirstIdentityToString(ctx);
165         if (request == null) {
166             AuditLogger.logItem(this.getClass().getCanonicalName(), "getMyScheduledReports", currentUser, "", "not specified", ctx.getMessageContext());
167             throw new IllegalArgumentException("request is null");
168         }
169         Utility.validateClassification(request.getClassification());
170 
171         AuditLogger.logItem(this.getClass().getCanonicalName(), "getMyScheduledReports", currentUser, "", (request.getClassification()), ctx.getMessageContext());
172         //log.log(Level.INFO, name + "exportDataToHTML" + currentUser);
173         //no authorization is necessary here
174 
175         if (request.getRecordlimit() == 0) {
176             request.setRecordlimit(100);
177         }
178         //when reading the database by sure to use the column to override the lastupdated field
179         GetMyScheduledReportsResponseMsg ret = new GetMyScheduledReportsResponseMsg();
180         Connection con = null;
181         PreparedStatement cmd = null;
182         ResultSet rs = null;
183         try {
184             ret.setClassification(getCurrentOperatingClassificationLevel());
185             JAXBContext GetARSSerializationContext = Utility.getARSSerializationContext();
186             Unmarshaller u = GetARSSerializationContext.createUnmarshaller();
187             con = Utility.getPerformanceDBConnection();
188             cmd = con.prepareStatement("select * from arsjobs where owninguser=?;");
189             cmd.setString(1, currentUser);
190             rs = cmd.executeQuery();
191             while (rs.next()) {
192                 ExistingReportDefitions ed = new ExistingReportDefitions();
193                 byte[] s = rs.getBytes("reportdef");
194                 ByteArrayInputStream bss = new ByteArrayInputStream(s);
195                 XMLInputFactory xf = XMLInputFactory.newInstance();
196                 XMLStreamReader r = xf.createXMLStreamReader(bss);
197                 JAXBElement<ReportDefinition> foo = (JAXBElement<ReportDefinition>) u.unmarshal(r, ReportDefinition.class);
198                 if (foo != null && foo.getValue() != null) {
199                     ed.setJob(foo.getValue());
200                 }
201                 ed.getJob().setLastRanAt(ConvertToXmlGreg(rs.getLong("lastranat")));
202                 ret.getCompletedJobs().add(ed);
203             }
204         } catch (Exception ex) {
205             log.log(Level.ERROR, null, ex);
206         } finally {
207             DBUtils.safeClose(rs);
208             DBUtils.safeClose(cmd);
209             DBUtils.safeClose(con);
210         }
211         con = null;
212 
213         try {
214             con = Utility.getPerformanceDBConnection();
215 
216             //check for completed jobs
217             for (int i = 0; i < ret.getCompletedJobs().size(); i++) {
218 
219                 cmd = con.prepareStatement("select * from arsreports where jobid=? limit  ? offset ?");
220                 cmd.setString(1, ret.getCompletedJobs().get(i).getJob().getJobId());
221                 cmd.setInt(2, request.getRecordlimit());
222                 cmd.setInt(3, request.getOffset());
223                 rs = cmd.executeQuery();
224                 while (rs.next()) {
225                     CompletedJobs cj = new CompletedJobs();
226                     cj.setReportId(rs.getString("reportid"));
227                     cj.setTimestamp(ConvertToXmlGreg(rs.getLong("datetime")));
228                     ret.getCompletedJobs().get(i).getReports().add(cj);
229                 }
230 
231                 rs.close();
232                 cmd.close();
233             }
234             return ret;
235         } catch (Exception ex) {
236             log.log(Level.ERROR, null, ex);
237         } finally {
238             DBUtils.safeClose(rs);
239             DBUtils.safeClose(cmd);
240             DBUtils.safeClose(con);
241         }
242         ServiceUnavailableException f = new ServiceUnavailableException("", null);
243         f.getFaultInfo().setCode(ServiceUnavailableFaultCodes.DATA_BASE_UNAVAILABLE);
244         throw f;
245 
246     }
247 
248     private ReportDefinition loadReportDef(String job) throws Exception {
249         Connection con = null;
250         PreparedStatement cmd = null;
251         ResultSet rs = null;
252         try {
253             ReportDefinition ret = null;
254             JAXBContext GetARSSerializationContext = Utility.getARSSerializationContext();
255             Unmarshaller u = GetARSSerializationContext.createUnmarshaller();
256             con = Utility.getPerformanceDBConnection();
257             cmd = con.prepareStatement("select * from arsjobs where jobid=?;");
258             cmd.setString(1, job);
259             rs = cmd.executeQuery();
260             if (rs.next()) {
261 
262                 byte[] s = rs.getBytes("reportdef");
263                 ByteArrayInputStream bss = new ByteArrayInputStream(s);
264                 XMLInputFactory xf = XMLInputFactory.newInstance();
265                 XMLStreamReader r = xf.createXMLStreamReader(bss);
266                 JAXBElement<ReportDefinition> foo = (JAXBElement<ReportDefinition>) u.unmarshal(r, ReportDefinition.class);
267                 if (foo != null && foo.getValue() != null) {
268                     ret = foo.getValue();
269                 }
270 
271             }
272 
273             return ret;
274         } catch (Exception ex) {
275             log.log(Level.WARN, "loadReportDef error", ex);
276         } finally {
277             DBUtils.safeClose(rs);
278             DBUtils.safeClose(cmd);
279             DBUtils.safeClose(con);
280         }
281         throw new IllegalArgumentException("job not found");
282     }
283 
284     private SecurityWrapper getCurrentOperatingClassificationLevel() {
285         try {
286             SecurityWrapper t = getGlobalPolicyFromDB().getClassification();
287             log.log(Level.INFO, "PCS, current security classification is " + Utility.ICMClassificationToString(t.getClassification()) + " " + t.getCaveats());
288             return t;
289         } catch (Exception ex) {
290             log.log(Level.ERROR, "Unable to determine current classification level. Is the database available?", ex);
291         }
292         throw new IllegalAccessError();
293     }
294 
295     private GetGlobalPolicyResponseMsg getGlobalPolicyFromDB() throws org.miloss.fgsms.services.interfaces.policyconfiguration.ServiceUnavailableException, ServiceUnavailableException {
296 
297         Connection con = Utility.getConfigurationDBConnection();
298         PreparedStatement comm = null;
299         ResultSet results = null;
300         boolean foundPolicy = false;
301         GetGlobalPolicyResponseMsg response = new GetGlobalPolicyResponseMsg();
302         try {
303             response.setPolicy(new GlobalPolicy());
304             //we don't care about the request in this case
305 
306             comm = con.prepareStatement("Select * from GlobalPolicies;");
307 
308             /////////////////////////////////////////////
309             //get the global policy for data retension
310             /////////////////////////////////////////////
311             results = comm.executeQuery();
312             //       int count = 0;
313 
314             if (results.next()) {
315                 response.getPolicy().setPolicyRefreshRate(df.newDuration(results.getLong("PolicyRefreshRate")));
316                 response.getPolicy().setRecordedMessageCap(results.getInt("RecordedMessageCap"));
317                 SecurityWrapper wrap = new SecurityWrapper(ClassificationType.fromValue(results.getString("classification")),
318                         results.getString("caveat"));
319                 response.setClassification(wrap);
320                 response.getPolicy().setAgentsEnabled(results.getBoolean("agentsenable"));
321                 foundPolicy = true;
322             }
323         } catch (Exception ex) {
324             log.log(Level.ERROR, "error getting global policy", ex);
325             ServiceUnavailableException f = new ServiceUnavailableException("", null);
326             f.getFaultInfo().setCode(ServiceUnavailableFaultCodes.DATA_BASE_UNAVAILABLE);
327             throw f;
328         } finally {
329             DBUtils.safeClose(results);
330             DBUtils.safeClose(comm);
331             DBUtils.safeClose(con);
332         }
333 
334         try {
335             if (!foundPolicy) {
336                 try {
337                     comm = con.prepareStatement("INSERT INTO GlobalPolicies (PolicyRefreshRate, RecordedMessageCap, classification, agentsenable, caveat) "
338                             + " VALUES (?, ?, ?, true, ?);");
339                     comm.setLong(1, 30 * 60 * 100);
340                     comm.setLong(2, 1024000);
341                     comm.setString(3, "U");
342                     comm.setString(4, "");
343                     comm.execute();
344                     response.getPolicy().setRecordedMessageCap(1024000);
345                     response.getPolicy().setClassification(new SecurityWrapper(ClassificationType.U, ""));
346                     response.getPolicy().setAgentsEnabled(true);
347                 } catch (SQLException ex) {
348                     log.log(Level.ERROR, "error setting global policy", ex);
349                 }
350             }
351             KeyNameValueEnc d = DBSettingsLoader.GetPropertiesFromDB(true, "UddiPublisher", "Interval");
352             if (d != null && d.getKeyNameValue() != null) {
353                 try {
354                     response.getPolicy().setUDDIPublishRate(df.newDuration(Long.parseLong(d.getKeyNameValue().getPropertyValue())));
355                 } catch (Exception ex) {
356                     response.getPolicy().setUDDIPublishRate(df.newDuration(30 * 60 * 100));
357                 }
358             }
359             //response.setClassification(getCurrentOperatingClassificationLevel());
360             return response;
361         } catch (Exception ex) {
362             log.log(Level.ERROR, "error getting global policy", ex);
363             ServiceUnavailableException f = new ServiceUnavailableException("", null);
364             f.getFaultInfo().setCode(ServiceUnavailableFaultCodes.DATA_BASE_UNAVAILABLE);
365             throw f;
366         } finally {
367             DBUtils.safeClose(comm);
368             DBUtils.safeClose(con);
369         }
370 
371     }
372 
373     /**
374      *
375      * @param request
376      * @return returns
377      * org.miloss.fgsms.services.interfaces.automatedreportingservice.AddOrUpdateScheduledReportResponseMsg
378      * @throws AccessDeniedException
379      * @throws ServiceUnavailableException
380      */
381     @WebMethod(operationName = "AddOrUpdateScheduledReport", action = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService/AddOrUpdateScheduledReport")
382     @WebResult(name = "response", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService")
383     @RequestWrapper(localName = "AddOrUpdateScheduledReport", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.AddOrUpdateScheduledReport")
384     @ResponseWrapper(localName = "AddOrUpdateScheduledReportResponse", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.AddOrUpdateScheduledReportResponse")
385     public AddOrUpdateScheduledReportResponseMsg addOrUpdateScheduledReport(
386             @WebParam(name = "request", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService") AddOrUpdateScheduledReportRequestMsg request)
387             throws AccessDeniedException, ServiceUnavailableException {
388         String currentUser = UserIdentityUtil.getFirstIdentityToString(ctx);
389         if (request == null) {
390             AuditLogger.logItem(this.getClass().getCanonicalName(), "addOrUpdateScheduledReport", currentUser, "", "not specified", ctx.getMessageContext());
391             throw new IllegalArgumentException("request is null");
392         }
393         Utility.validateClassification(request.getClassification());
394         if (request.getJobs().isEmpty()) {
395             throw new IllegalArgumentException("at least one report must be specified for updating");
396         }
397         AuditLogger.logItem(this.getClass().getCanonicalName(), "addOrUpdateScheduledReport", currentUser, "", (request.getClassification()), ctx.getMessageContext());
398 
399         //validate request
400         //loop through all jobs, add job ids if not present, authorize the request
401         validateRequest(request, currentUser, ctx);
402 
403         //     if (UserIdentityUtil.hasGlobalAdministratorRole(currentUser, "addOrUpdateScheduledReport", request.getClassification(), ctx))
404         //TODO admins?
405         Connection con = Utility.getPerformanceDBConnection();
406         for (int i = 0; i < request.getJobs().size(); i++) {
407             PreparedStatement cmd = null;
408             try {
409 
410                 StringWriter sw = new StringWriter();
411 
412                 if (!Utility.stringIsNullOrEmpty(request.getJobs().get(i).getJobId())) {
413                     cmd = con.prepareStatement("UPDATE arsjobs SET  reportdef=?, hasextrapermissions=?, enabled=? WHERE jobid=?;");
414                     //cmd.setBytes(1, bits);
415                     cmd.setBoolean(2, !request.getJobs().get(i).getAdditionalReaders().isEmpty());
416                     cmd.setBoolean(3, request.getJobs().get(i).isEnabled());
417                     cmd.setString(4, request.getJobs().get(i).getJobId());
418                 } else {
419                     request.getJobs().get(i).setJobId(UUID.randomUUID().toString());
420                     cmd = con.prepareStatement("INSERT INTO arsjobs (reportdef, hasextrapermissions,  enabled, lastranat, jobid, owninguser) values (?,?,?,?,?,?);");
421 
422                     cmd.setBoolean(2, !request.getJobs().get(i).getAdditionalReaders().isEmpty());
423 
424                     cmd.setBoolean(3, request.getJobs().get(i).isEnabled());
425                     cmd.setLong(4, 0);
426                     cmd.setString(5, request.getJobs().get(i).getJobId());
427                     cmd.setString(6, currentUser);
428                 }
429                 Utility.getARSSerializationContext().createMarshaller().marshal(request.getJobs().get(i), sw);
430                 byte[] bits = sw.toString().getBytes(Constants.CHARSET);
431                 cmd.setBytes(1, bits);
432                 cmd.executeUpdate();
433                 cmd.close();
434             } catch (Exception ex) {
435                 log.log(Level.ERROR, null, ex);
436             } finally {
437                 DBUtils.safeClose(cmd);
438             }
439         }
440         if (con != null) {
441             DBUtils.safeClose(con);
442         }
443         AddOrUpdateScheduledReportResponseMsg r = new AddOrUpdateScheduledReportResponseMsg();
444         r.setClassification(getCurrentOperatingClassificationLevel());
445         r.getJobs().addAll(request.getJobs());
446         r.setSuccess(true);
447         r.setMessage(OK);
448         return r;
449 
450     }
451 
452     /**
453      ** This will delete the job AND all reports associated with the job
454      *
455      * @param request
456      * @return returns
457      * org.miloss.fgsms.services.interfaces.automatedreportingservice.DeleteScheduledReportResponseMsg
458      * @throws AccessDeniedException
459      * @throws ServiceUnavailableException
460      */
461     @WebMethod(operationName = "DeleteScheduledReport", action = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService/DeleteScheduledReport")
462     @WebResult(name = "response", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService")
463     @RequestWrapper(localName = "DeleteScheduledReport", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.DeleteScheduledReport")
464     @ResponseWrapper(localName = "DeleteScheduledReportResponse", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.DeleteScheduledReportResponse")
465     public DeleteScheduledReportResponseMsg deleteScheduledReport(
466             @WebParam(name = "request", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService") DeleteScheduledReportRequestMsg request)
467             throws AccessDeniedException, ServiceUnavailableException {
468         String currentUser = UserIdentityUtil.getFirstIdentityToString(ctx);
469         if (request == null) {
470             AuditLogger.logItem(this.getClass().getCanonicalName(), "deleteScheduledReport", currentUser, "", "not specified", ctx.getMessageContext());
471             throw new IllegalArgumentException("request is null");
472         }
473         Utility.validateClassification(request.getClassification());
474 
475         if (Utility.stringIsNullOrEmpty(request.getJobId())) {
476             throw new IllegalArgumentException("a job id must be specified");
477         }
478         AuditLogger.logItem(this.getClass().getCanonicalName(), "deleteScheduledReport", currentUser, request.getJobId(), (request.getClassification()), ctx.getMessageContext());
479 
480         Connection con = null;
481         PreparedStatement cmd = null;
482         try {
483 
484             con = Utility.getPerformanceDBConnection();
485 
486             if (UserIdentityUtil.hasGlobalAdministratorRole(currentUser, "deleteScheduledReport", request.getClassification(), ctx)) {
487                 cmd = con.prepareStatement("delete from arsjobs where jobid=? ");
488                 cmd.setString(1, request.getJobId());
489             } else {
490                 cmd = con.prepareStatement("delete from arsjobs where owninguser=? and jobid=? ");
491                 cmd.setString(1, currentUser);
492                 cmd.setString(2, request.getJobId());
493             }
494             int jobsdeleted = cmd.executeUpdate();
495             cmd.close();
496             if (jobsdeleted == 0) {
497                 con.close();
498                 throw new AccessDeniedException("either the job doesn't exist or you don't own it", new org.miloss.fgsms.services.interfaces.faults.AccessDeniedException());
499             }
500 
501             cmd = con.prepareStatement("delete from arsreports where jobid=? ");
502             cmd.setString(1, request.getJobId());
503             cmd.executeUpdate();
504 
505             DeleteScheduledReportResponseMsg r = new DeleteScheduledReportResponseMsg();
506             r.setClassification(getCurrentOperatingClassificationLevel());
507             r.setSuccess(true);
508             r.setMessage(OK);
509             return r;
510         } catch (SQLException ex) {
511             log.log(Level.ERROR, null, ex);
512 
513             ServiceUnavailableException f = new ServiceUnavailableException("", null);
514 
515             f.getFaultInfo().setCode(ServiceUnavailableFaultCodes.DATA_BASE_UNAVAILABLE);
516             throw f;
517         } finally {
518             DBUtils.safeClose(cmd);
519             DBUtils.safeClose(con);
520         }
521     }
522 
523     /**
524      * Gets a specific generated report
525      *
526      * @param request
527      * @return returns
528      * org.miloss.fgsms.services.interfaces.automatedreportingservice.GetReportResponseMsg
529      * @throws AccessDeniedException
530      * @throws ServiceUnavailableException
531      */
532     @WebMethod(operationName = "GetReport", action = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService/GetReport")
533     @WebResult(name = "response", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService")
534     @RequestWrapper(localName = "GetReport", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.GetReport")
535     @ResponseWrapper(localName = "GetReportResponse", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.GetReportResponse")
536     public GetReportResponseMsg getReport(
537             @WebParam(name = "request", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService") GetReportRequestMsg request)
538             throws AccessDeniedException, ServiceUnavailableException {
539         String currentUser = UserIdentityUtil.getFirstIdentityToString(ctx);
540         if (request == null) {
541             AuditLogger.logItem(this.getClass().getCanonicalName(), "getReport", currentUser, "", "not specified", ctx.getMessageContext());
542             throw new IllegalArgumentException("request is null");
543         }
544         Utility.validateClassification(request.getClassification());
545         if (Utility.stringIsNullOrEmpty(request.getReportId())) {
546             throw new IllegalArgumentException("a report id must be specified");
547         }
548         AuditLogger.logItem(this.getClass().getCanonicalName(), "getReport", currentUser, request.getReportId(), (request.getClassification()), ctx.getMessageContext());
549         GetReportResponseMsg r = new GetReportResponseMsg();
550         r.setClassification(getCurrentOperatingClassificationLevel());
551         r.setReportId(request.getReportId());
552         r.setZippedReport(get_a_Report(request.getReportId(), currentUser, request.getClassification()));
553         if (r.getZippedReport() == null) {
554             throw new IllegalArgumentException("report not found");
555         }
556         return r;
557     }
558 
559     protected byte[] get_a_Report(final String id, final String currentUser, SecurityWrapper wrapper) throws AccessDeniedException {
560         String job = null;
561         boolean access = false;
562         Connection con = null;
563         ResultSet rs = null;
564         PreparedStatement cmd = null;
565         try {
566             con = Utility.getPerformanceDBConnection();
567             cmd = con.prepareStatement("select jobid from arsreports where reportid=?");
568             cmd.setString(1, id);
569             rs = cmd.executeQuery();
570             if (rs.next()) {
571                 job = rs.getString(1);
572             }
573         } catch (Exception ex) {
574             log.log(Level.ERROR, null, ex);
575             if (ex instanceof AccessDeniedException) {
576                 throw (AccessDeniedException) ex;
577             }
578         } finally {
579             DBUtils.safeClose(rs);
580             DBUtils.safeClose(cmd);
581             DBUtils.safeClose(con);
582         }
583         try {
584 
585             if (!Utility.stringIsNullOrEmpty(job)) {
586                 ReportDefinition rd = loadReportDef(job);
587                 if (rd.getOwner().equalsIgnoreCase(currentUser)) {
588                     access = true;
589                 } else {
590                     for (int i = 0; i < rd.getAdditionalReaders().size(); i++) {
591                         if (currentUser.equalsIgnoreCase(rd.getAdditionalReaders().get(i))) {
592                             access = true;
593                         }
594                     }
595                 }
596                 if (access) {
597                     //get the report
598                     con = Utility.getPerformanceDBConnection();
599                     cmd = con.prepareStatement("select reportcontents from arsreports where reportid=?");
600                     cmd.setString(1, id);
601                     rs = cmd.executeQuery();
602                     byte[] bits = null;
603                     if (rs.next()) {
604                         bits = rs.getBytes(1);
605                     }
606                     if (bits != null) {
607                         return bits;
608                     }
609                 } else {
610                     throw new AccessDeniedException("", new org.miloss.fgsms.services.interfaces.faults.AccessDeniedException());
611                 }
612             } else {
613                 //report not found
614                 return null;
615             }
616         } catch (Exception ex) {
617 
618             if (ex instanceof AccessDeniedException) {
619                 AuditLogger.logItem(this.getClass().getCanonicalName(), "getReport", currentUser, "FAILURE, attempt to access ARS report " + id, wrapper, ctx.getMessageContext());
620                 throw (AccessDeniedException) ex;
621             }
622             log.log(Level.ERROR, null, ex);
623         } finally {
624             DBUtils.safeClose(rs);
625             DBUtils.safeClose(cmd);
626             DBUtils.safeClose(con);
627         }
628         return null;
629     }
630 
631     /**
632      ** Deletes a generated report
633      *
634      * @param request
635      * @return returns
636      * org.miloss.fgsms.services.interfaces.automatedreportingservice.DeleteReportResponseMsg
637      * @throws AccessDeniedException
638      * @throws ServiceUnavailableException
639      */
640     @WebMethod(operationName = "DeleteReport", action = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService/DeleteReport")
641     @WebResult(name = "response", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService")
642     @RequestWrapper(localName = "DeleteReport", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.DeleteReport")
643     @ResponseWrapper(localName = "DeleteReportResponse", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService", className = "org.miloss.fgsms.services.interfaces.automatedreportingservice.DeleteReportResponse")
644     public DeleteReportResponseMsg deleteReport(
645             @WebParam(name = "request", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:automatedReportingService") DeleteReportRequestMsg request)
646             throws AccessDeniedException, ServiceUnavailableException {
647         String currentUser = UserIdentityUtil.getFirstIdentityToString(ctx);
648         if (request == null) {
649             AuditLogger.logItem(this.getClass().getCanonicalName(), "deleteReport", currentUser, "", "not specified", ctx.getMessageContext());
650             throw new IllegalArgumentException("request is null");
651         }
652         Utility.validateClassification(request.getClassification());
653 
654         if (Utility.stringIsNullOrEmpty(request.getReportId())) {
655             throw new IllegalArgumentException("a report id must be specified");
656         }
657         AuditLogger.logItem(this.getClass().getCanonicalName(), "deleteReport", currentUser, request.getReportId(), (request.getClassification()), ctx.getMessageContext());
658         boolean proceed = false;
659         Connection con = null;
660         PreparedStatement cmd = null;
661         ResultSet rs = null;
662         try {
663             con = Utility.getPerformanceDBConnection();
664             cmd = con.prepareStatement("select owninguser from arsjobs, arsreports where arsjobs.jobid=arsreports.jobid and arsreports.reportid=?;");
665             cmd.setString(1, request.getReportId());
666             rs = cmd.executeQuery();
667             if (rs.next()) {
668                 String owner = rs.getString(1);
669                 if (!Utility.stringIsNullOrEmpty(owner)) {
670                     if (currentUser.equalsIgnoreCase(owner)) {
671                         proceed = true;
672                     }
673                 }
674             }
675 
676         } catch (Exception ex) {
677             //record not found
678             log.log(Level.ERROR, "error getting global policy", ex);
679             ServiceUnavailableException f = new ServiceUnavailableException("", null);
680             f.getFaultInfo().setCode(ServiceUnavailableFaultCodes.DATA_BASE_UNAVAILABLE);
681             throw f;
682         } finally {
683             DBUtils.safeClose(rs);
684             DBUtils.safeClose(cmd);
685             DBUtils.safeClose(con);
686         }
687 
688         if (proceed) {
689             try {
690                 con = Utility.getPerformanceDBConnection();
691                 cmd = con.prepareStatement("delete from arsreports where arsreports.reportid=?;");
692                 cmd.setString(1, request.getReportId());
693                 int k = cmd.executeUpdate();
694 
695                 if (k != 1) {
696                     ServiceUnavailableException f = new ServiceUnavailableException("Deletion failed", null);
697                     f.getFaultInfo().setCode(ServiceUnavailableFaultCodes.UNEXPECTED_ERROR);
698                     throw f;
699                 }
700                 DeleteReportResponseMsg ret = new DeleteReportResponseMsg();
701                 ret.setSuccess(true);
702                 ret.setMessage(OK);
703                 ret.setClassification(getCurrentOperatingClassificationLevel());
704                 return ret;
705             } catch (Exception ex) {
706 
707                 log.log(Level.ERROR, "error removing reporting", ex);
708                 ServiceUnavailableException f = new ServiceUnavailableException("", null);
709                 f.getFaultInfo().setCode(ServiceUnavailableFaultCodes.DATA_BASE_UNAVAILABLE);
710                 throw f;
711             } finally {
712                 DBUtils.safeClose(rs);
713                 DBUtils.safeClose(cmd);
714                 DBUtils.safeClose(con);
715 
716             }
717         } else //access denied you don't own this job or it doesn't exist
718         {
719             log.log(Level.ERROR, "cannot remove report, either the job or report doesn't exist or the current user doesn't own it " + currentUser);
720             AccessDeniedException f = new AccessDeniedException("", null);
721             throw f;
722         }
723     }
724 
725     private Calendar ConvertToXmlGreg(long aLong) {
726         if (aLong == 0) {
727             return null;
728         }
729         GregorianCalendar gcal = new GregorianCalendar();
730         gcal.setTimeInMillis(aLong);
731         return (gcal);
732     }
733 
734     private static boolean IsReportJobOwner(String currentUser, String jobId) {
735         Connection con = null;
736         boolean ok = false;
737         PreparedStatement cmd = null;
738         ResultSet rs = null;
739         try {
740             con = Utility.getPerformanceDBConnection();
741             cmd = con.prepareStatement("select * from arsjobs where jobid=? and owninguser=?");
742             cmd.setString(1, jobId);
743             cmd.setString(2, currentUser);
744             rs = cmd.executeQuery();
745             if (rs.next()) {
746                 ok = true;
747             }
748 
749         } catch (Exception ex) {
750             log.log(Level.ERROR, "error caught searching for a report job", ex);
751         } finally {
752             DBUtils.safeClose(rs);
753             DBUtils.safeClose(cmd);
754             DBUtils.safeClose(con);
755         }
756         return ok;
757     }
758 
759     private static void assertNotNull(Object exportType) {
760         if (exportType == null) {
761             throw new IllegalArgumentException("a required value is null");
762         }
763     }
764 
765     private static void assertFalse(boolean empty) {
766         if (empty) {
767             throw new IllegalArgumentException("The array is empty");
768         }
769     }
770 
771     private static void validateRange(TimeRangeDiff range) {
772         if (range == null) {
773             throw new IllegalArgumentException("The time range diff is empty");
774         }
775         if (range.getStart() == null) {
776             throw new IllegalArgumentException("The time range diff start is empty");
777         }
778         if (range.getEnd() == null) {
779             throw new IllegalArgumentException("The time range diff endis empty");
780         }
781         if (range.getStart().equals(range.getEnd()) || range.getStart().isShorterThan(range.getEnd())) {
782             throw new IllegalArgumentException("The time range diff is invalid, start should be a longer duration that end");
783         }
784     }
785 
786     private static void validReportDefinition(ReportDefinition get) {
787         if (get == null) {
788             throw new IllegalArgumentException("The report def is empty");
789         }
790         if (get.getSchedule() == null) {
791             throw new IllegalArgumentException("The schedule is empty");
792         }
793         if (get.getSchedule().getTriggers().isEmpty()) {
794             throw new IllegalArgumentException("The report def has no triggers defined.");
795         }
796 
797         for (int i = 0; i < get.getSchedule().getTriggers().size(); i++) {
798             if (get.getSchedule().getTriggers().get(i).getStartingAt() == null) {
799                 throw new IllegalArgumentException("The report def has invalid triggers.");
800             }
801 
802             if (get.getSchedule().getTriggers().get(i) instanceof DailySchedule) {
803                 DailySchedule ds = (DailySchedule) get.getSchedule().getTriggers().get(i);
804                 if (ds.getReoccurs() == null || ds.getReoccurs().intValue() < 1) {
805                     throw new IllegalArgumentException("The report def has invalid value for reoccuring.");
806                 }
807             } else if (get.getSchedule().getTriggers().get(i) instanceof WeeklySchedule) {
808                 WeeklySchedule ds = (WeeklySchedule) get.getSchedule().getTriggers().get(i);
809                 if (ds.getDayOfTheWeekIs().isEmpty()) {
810                     throw new IllegalArgumentException("The report def has invalid weekly schedule for day of the weeks.");
811                 }
812                 if (ds.getReoccurs() == null || ds.getReoccurs().intValue() < 1) {
813                     throw new IllegalArgumentException("The report def has invalid value for reoccuring.");
814                 }
815             } else if (get.getSchedule().getTriggers().get(i) instanceof MonthlySchedule) {
816                 MonthlySchedule ds = (MonthlySchedule) get.getSchedule().getTriggers().get(i);
817                 if (ds.getDayOfTheMonthIs().isEmpty()) {
818                     throw new IllegalArgumentException("The report def has invalid weekly schedule for day of the month.");
819                 }
820                 if (ds.getMonthNameIs().isEmpty()) {
821                     throw new IllegalArgumentException("The report def has invalid weekly schedule for month.");
822                 }
823             } else if (get.getSchedule().getTriggers().get(i) instanceof OneTimeSchedule) {
824                 //TODO
825             } else if (get.getSchedule().getTriggers().get(i) instanceof ImmediateSchedule) {
826                 //TODO
827             } else {
828                 throw new IllegalArgumentException("The report def has a trigger that is not intrepretable.");
829             }
830         }
831 
832         if (get.getExportCSVDataRequestMsg() != null) {
833             Utility.validateClassification(get.getExportCSVDataRequestMsg().getClassification());
834         }
835         if (get.getExportDataRequestMsg() != null) {
836             Utility.validateClassification(get.getExportDataRequestMsg().getClassification());
837         }
838     }
839 
840     private static void validateRequest(AddOrUpdateScheduledReportRequestMsg request, String currentUser, WebServiceContext ctx) throws AccessDeniedException {
841         for (int i = 0; i < request.getJobs().size(); i++) {
842             request.getJobs().get(i).setOwner(currentUser);
843             if (Utility.stringIsNullOrEmpty(request.getJobs().get(i).getJobId())) {
844                 //its a new job
845                 //request.getJobs().get(i).setJobId(UUID.randomUUID().toString());
846             } else {
847                 //existing job
848                 //prevent users from overriding existing reports that are owned by others
849                 if (!IsReportJobOwner(currentUser, request.getJobs().get(i).getJobId())) {
850                     AccessDeniedException f = new AccessDeniedException("the report job " + request.getJobs().get(i).getJobId() + " is not owned by you", null);
851                     throw f;
852                 }
853             }
854             if (request.getJobs().get(i).getExportCSVDataRequestMsg() == null && request.getJobs().get(i).getExportDataRequestMsg() == null) {
855                 throw new IllegalArgumentException("one of ExportData or ExportCSV must be specified");
856             }
857             if (request.getJobs().get(i).getExportCSVDataRequestMsg() != null && request.getJobs().get(i).getExportDataRequestMsg() != null) {
858                 throw new IllegalArgumentException("both ExportData and ExportCSV cannot be specified on the same report definition");
859             }
860 
861             validReportDefinition(request.getJobs().get(i));
862             validateReportAlerts(request.getJobs().get(i));
863             if (request.getJobs().get(i).getExportCSVDataRequestMsg() != null) {
864                 if (request.getJobs().get(i).getExportCSVDataRequestMsg().getExportType() != ExportRecordsEnum.AUDIT_LOGS
865                         && request.getJobs().get(i).getExportCSVDataRequestMsg().getURLs().isEmpty()) {
866                     throw new IllegalArgumentException("ExportCSV requires at least one URL when not requesting audit logs");
867                 }
868                 for (int k = 0; k < request.getJobs().get(i).getExportCSVDataRequestMsg().getURLs().size(); k++) {
869                     if (request.getJobs().get(i).getExportCSVDataRequestMsg().getExportType() == ExportRecordsEnum.TRANSACTIONS) {
870                         UserIdentityUtil.assertAuditAccess(request.getJobs().get(i).getExportCSVDataRequestMsg().getURLs().get(k), currentUser, "addOrUpdateScheduledReport", request.getClassification(), ctx);
871                     } else {
872                         UserIdentityUtil.assertReadAccess(request.getJobs().get(i).getExportCSVDataRequestMsg().getURLs().get(k), currentUser, "addOrUpdateScheduledReport", request.getClassification(), ctx);
873                     }
874                 }
875                 assertNotNull(request.getJobs().get(i).getExportCSVDataRequestMsg().getExportType());
876                 assertNotNull(request.getJobs().get(i).getExportCSVDataRequestMsg().getRange());
877                 assertNotNull(request.getJobs().get(i).getExportCSVDataRequestMsg().getRange().getEnd());
878                 assertNotNull(request.getJobs().get(i).getExportCSVDataRequestMsg().getRange().getStart());
879                 validateRange(request.getJobs().get(i).getExportCSVDataRequestMsg().getRange());
880                 if (request.getJobs().get(i).getExportCSVDataRequestMsg().getExportType() == ExportRecordsEnum.AUDIT_LOGS) {
881                     UserIdentityUtil.assertGlobalAuditRole(currentUser, "ValidReportDefinition", request.getClassification(), ctx);
882                 }
883             }
884             if (request.getJobs().get(i).getExportDataRequestMsg() != null) {
885                 for (int k = 0; k < request.getJobs().get(i).getExportDataRequestMsg().getURLs().size(); k++) {
886                     UserIdentityUtil.assertReadAccess(request.getJobs().get(i).getExportDataRequestMsg().getURLs().get(k), currentUser, "addOrUpdateScheduledReport", request.getClassification(), ctx);
887                 }
888 
889                 assertNotNull(request.getJobs().get(i).getExportDataRequestMsg().getReportTypes());
890                 assertNotNull(request.getJobs().get(i).getExportDataRequestMsg().getReportTypes().getReportTypeContainer());
891                 assertFalse(request.getJobs().get(i).getExportDataRequestMsg().getReportTypes().getReportTypeContainer().isEmpty());
892                 //validate that
893                 for (int k = 0; k < request.getJobs().get(i).getExportDataRequestMsg().getReportTypes().getReportTypeContainer().size(); k++) {
894                     ReportTypeContainer reportType = request.getJobs().get(i).getExportDataRequestMsg().getReportTypes().getReportTypeContainer().get(i);
895                     try {
896                         //validate that the plugin is registered.
897                         validatePluginRegistered(reportType.getType());
898                     } catch (Exception ex) {
899                         log.warn(null, ex);
900                         throw new IllegalArgumentException(ex.getMessage());
901                     }
902                     //validate that the plugin is infact loadable
903                     try {
904                         ReportGeneratorPlugin plugin = (ReportGeneratorPlugin) Class.forName(reportType.getType()).newInstance();
905                     } catch (Throwable t) {
906                         log.warn(null, t);
907                         throw new IllegalArgumentException(reportType.getType() + " could not be initialized");
908                     }
909                 }
910                 assertNotNull(request.getJobs().get(i).getExportDataRequestMsg().getRange());
911                 assertNotNull(request.getJobs().get(i).getExportDataRequestMsg().getRange().getEnd());
912                 assertNotNull(request.getJobs().get(i).getExportDataRequestMsg().getRange().getStart());
913                 validateRange(request.getJobs().get(i).getExportDataRequestMsg().getRange());
914 
915             }
916         }
917     }
918 
919     private static void validateReportAlerts(ReportDefinition get) {
920         if (get.getNotifications().isEmpty()) {
921             return;
922         }
923         //FIXME
924         /*
925         for (int i = 0; i < get.getNotifications().size(); i++) {
926             if (get.getNotifications().get(i) instanceof SLAActionEmail) {
927                 throw new IllegalArgumentException("SLA Email actions cannot be used in this context");
928             }
929             if (get.getNotifications().get(i) instanceof SLAActionRestart) {
930                 throw new IllegalArgumentException("SLA Restart actions cannot be used in this context");
931             }
932 
933         }*/
934     }
935 
936     /**
937      * Get the operating status of this service
938      *
939      * @param request
940      * @return returns
941      * org.miloss.fgsms.services.interfaces.common.GetOperatingStatusResponseMessage
942      */
943     @WebMethod(operationName = "GetOperatingStatus", action = "urn:org:miloss:fgsms:services:interfaces:opStatusService/GetOperatingStatus")
944     @WebResult(name = "response", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:common")
945     @RequestWrapper(localName = "GetOperatingStatus", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:common", className = "org.miloss.fgsms.services.interfaces.common.GetOperatingStatus")
946     @ResponseWrapper(localName = "GetOperatingStatusResponse", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:common", className = "org.miloss.fgsms.services.interfaces.common.GetOperatingStatusResponse")
947     public GetOperatingStatusResponseMessage getOperatingStatus(
948             @WebParam(name = "request", targetNamespace = "urn:org:miloss:fgsms:services:interfaces:common") GetOperatingStatusRequestMessage request) {
949         String currentUser = UserIdentityUtil.getFirstIdentityToString(ctx);
950 
951         Utility.validateClassification(request.getClassification());
952         AuditLogger.logItem(this.getClass().getCanonicalName(), "getOperatingStatus", currentUser, "", (request.getClassification()), ctx.getMessageContext());
953 
954         GetOperatingStatusResponseMessage res = new GetOperatingStatusResponseMessage();
955 
956         res.setClassification(request.getClassification());
957         res.setVersionInfo(new GetOperatingStatusResponseMessage.VersionInfo());
958         res.getVersionInfo().setVersionData(org.miloss.fgsms.common.Constants.Version);
959         res.getVersionInfo().setVersionSource(org.miloss.fgsms.common.Constants.class.getCanonicalName());
960         res.setStartedAt(started);
961         boolean ok = true;
962         Connection con = Utility.getConfigurationDBConnection();
963         Connection con2 = Utility.getPerformanceDBConnection();
964         PreparedStatement prepareStatement = null;
965         PreparedStatement prepareStatement2 = null;
966         try {
967             prepareStatement = con.prepareStatement("select 1=1;");
968             prepareStatement.execute();
969             prepareStatement.close();
970 
971             prepareStatement2 = con2.prepareStatement("select 1=1;");
972             prepareStatement2.execute();
973             prepareStatement2.close();
974             res.setStatusMessage("OK");
975         } catch (Exception ex) {
976             log.log(Level.WARN, null, ex);
977             ok = false;
978             res.setStatusMessage("One or more of the database connections is available");
979         } finally {
980             DBUtils.safeClose(prepareStatement);
981             DBUtils.safeClose(prepareStatement2);
982 
983             DBUtils.safeClose(con2);
984             DBUtils.safeClose(con);
985         }
986         res.setStatus(ok);
987         return res;
988     }
989 }