1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "cloud_print/gcp20/prototype/print_job_handler.h"
6
7#include "base/bind.h"
8#include "base/command_line.h"
9#include "base/files/file_util.h"
10#include "base/format_macros.h"
11#include "base/guid.h"
12#include "base/logging.h"
13#include "base/message_loop/message_loop.h"
14#include "base/rand_util.h"
15#include "base/strings/string_util.h"
16#include "base/strings/stringprintf.h"
17#include "base/time/time.h"
18#include "cloud_print/gcp20/prototype/gcp20_switches.h"
19
20namespace {
21
22const int kDraftExpirationSec = 10;
23const int kLocalPrintJobExpirationSec = 20;
24const int kErrorTimeoutSec = 30;
25
26// Errors simulation constants:
27const double kPaperJamProbability = 1.0;
28const int kTooManyDraftsTimeout = 10;
29const size_t kMaxDrafts = 5;
30
31const base::FilePath::CharType kJobsPath[] = FILE_PATH_LITERAL("printjobs");
32
33bool ValidateTicket(const std::string& ticket) {
34  return true;
35}
36
37std::string GenerateId() {
38  return base::StringToLowerASCII(base::GenerateGUID());
39}
40
41}  // namespace
42
43struct PrintJobHandler::LocalPrintJobExtended {
44  LocalPrintJobExtended(const LocalPrintJob& job, const std::string& ticket)
45      : data(job),
46        ticket(ticket),
47        state(LocalPrintJob::STATE_DRAFT) {}
48  LocalPrintJobExtended() : state(LocalPrintJob::STATE_DRAFT) {}
49  ~LocalPrintJobExtended() {}
50
51  LocalPrintJob data;
52  std::string ticket;
53  LocalPrintJob::State state;
54  base::Time expiration;
55};
56
57struct PrintJobHandler::LocalPrintJobDraft {
58  LocalPrintJobDraft() {}
59  LocalPrintJobDraft(const std::string& ticket, const base::Time& expiration)
60      : ticket(ticket),
61        expiration(expiration) {}
62  ~LocalPrintJobDraft() {}
63
64  std::string ticket;
65  base::Time expiration;
66};
67
68using base::StringPrintf;
69
70PrintJobHandler::PrintJobHandler() {
71}
72
73PrintJobHandler::~PrintJobHandler() {
74}
75
76LocalPrintJob::CreateResult PrintJobHandler::CreatePrintJob(
77    const std::string& ticket,
78    std::string* job_id_out,
79    // TODO(maksymb): Use base::TimeDelta for timeout values
80    int* expires_in_out,
81    // TODO(maksymb): Use base::TimeDelta for timeout values
82    int* error_timeout_out,
83    std::string* error_description) {
84  if (!ValidateTicket(ticket))
85    return LocalPrintJob::CREATE_INVALID_TICKET;
86
87  // Let's simulate at least some errors just for testing.
88  if (CommandLine::ForCurrentProcess()->HasSwitch(
89          switches::kSimulatePrintingErrors)) {
90    if (base::RandDouble() <= kPaperJamProbability) {
91      *error_description = "Paper jam, try again";
92      return LocalPrintJob::CREATE_PRINTER_ERROR;
93    }
94
95    if (drafts.size() > kMaxDrafts) {  // Another simulation of error: business
96      *error_timeout_out = kTooManyDraftsTimeout;
97      return LocalPrintJob::CREATE_PRINTER_BUSY;
98    }
99  }
100
101  std::string id = GenerateId();
102  drafts[id] = LocalPrintJobDraft(
103      ticket,
104      base::Time::Now() + base::TimeDelta::FromSeconds(kDraftExpirationSec));
105  base::MessageLoop::current()->PostDelayedTask(
106      FROM_HERE,
107      base::Bind(&PrintJobHandler::ForgetDraft, AsWeakPtr(), id),
108      base::TimeDelta::FromSeconds(kDraftExpirationSec));
109
110  *job_id_out = id;
111  *expires_in_out = kDraftExpirationSec;
112  return LocalPrintJob::CREATE_SUCCESS;
113}
114
115LocalPrintJob::SaveResult PrintJobHandler::SaveLocalPrintJob(
116    const LocalPrintJob& job,
117    std::string* job_id_out,
118    int* expires_in_out,
119    std::string* error_description_out,
120    int* timeout_out) {
121  std::string id;
122  int expires_in;
123
124  switch (CreatePrintJob(std::string(), &id, &expires_in,
125                         timeout_out, error_description_out)) {
126    case LocalPrintJob::CREATE_INVALID_TICKET:
127      NOTREACHED();
128      return LocalPrintJob::SAVE_SUCCESS;
129
130    case LocalPrintJob::CREATE_PRINTER_BUSY:
131      return LocalPrintJob::SAVE_PRINTER_BUSY;
132
133    case LocalPrintJob::CREATE_PRINTER_ERROR:
134      return LocalPrintJob::SAVE_PRINTER_ERROR;
135
136    case LocalPrintJob::CREATE_SUCCESS:
137      *job_id_out = id;
138      return CompleteLocalPrintJob(job, id, expires_in_out,
139                                   error_description_out, timeout_out);
140
141    default:
142      NOTREACHED();
143      return LocalPrintJob::SAVE_SUCCESS;
144  }
145}
146
147LocalPrintJob::SaveResult PrintJobHandler::CompleteLocalPrintJob(
148    const LocalPrintJob& job,
149    const std::string& job_id,
150    int* expires_in_out,
151    std::string* error_description_out,
152    int* timeout_out) {
153  if (!drafts.count(job_id)) {
154    *timeout_out = kErrorTimeoutSec;
155    return LocalPrintJob::SAVE_INVALID_PRINT_JOB;
156  }
157
158  std::string file_extension;
159  // TODO(maksymb): Gather together this type checking with Printer
160  // supported types in kCdd.
161  if (job.content_type == "application/pdf") {
162    file_extension = "pdf";
163  } else if (job.content_type == "image/pwg-raster") {
164    file_extension = "pwg";
165  } else if (job.content_type == "image/jpeg") {
166    file_extension = "jpg";
167  } else {
168    error_description_out->clear();
169    return LocalPrintJob::SAVE_INVALID_DOCUMENT_TYPE;
170  }
171  CompleteDraft(job_id, job);
172  std::map<std::string, LocalPrintJobExtended>::iterator current_job =
173      jobs.find(job_id);
174
175  if (!SavePrintJob(current_job->second.data.content,
176                    current_job->second.ticket,
177                    base::Time::Now(),
178                    StringPrintf("%s", job_id.c_str()),
179                    current_job->second.data.job_name, file_extension)) {
180    SetJobState(job_id, LocalPrintJob::STATE_ABORTED);
181    *error_description_out = "IO error";
182    return LocalPrintJob::SAVE_PRINTER_ERROR;
183  }
184
185  SetJobState(job_id, LocalPrintJob::STATE_DONE);
186  *expires_in_out = static_cast<int>(GetJobExpiration(job_id).InSeconds());
187  return LocalPrintJob::SAVE_SUCCESS;
188}
189
190bool PrintJobHandler::GetJobState(const std::string& id,
191                                  LocalPrintJob::Info* info_out) {
192  using base::Time;
193
194  std::map<std::string, LocalPrintJobDraft>::iterator draft = drafts.find(id);
195  if (draft != drafts.end()) {
196    info_out->state = LocalPrintJob::STATE_DRAFT;
197    info_out->expires_in =
198        static_cast<int>((draft->second.expiration - Time::Now()).InSeconds());
199    return true;
200  }
201
202  std::map<std::string, LocalPrintJobExtended>::iterator job = jobs.find(id);
203  if (job != jobs.end()) {
204    info_out->state = job->second.state;
205    info_out->expires_in = static_cast<int>(GetJobExpiration(id).InSeconds());
206    return true;
207  }
208  return false;
209}
210
211bool PrintJobHandler::SavePrintJob(const std::string& content,
212                                   const std::string& ticket,
213                                   const base::Time& create_time,
214                                   const std::string& id,
215                                   const std::string& title,
216                                   const std::string& file_extension) {
217  VLOG(1) << "Printing printjob: \"" + title + "\"";
218  base::FilePath directory(kJobsPath);
219
220  if (!base::DirectoryExists(directory) &&
221      !base::CreateDirectory(directory)) {
222    return false;
223  }
224
225  base::Time::Exploded create_time_exploded;
226  create_time.UTCExplode(&create_time_exploded);
227  base::FilePath::StringType job_prefix =
228      StringPrintf(FILE_PATH_LITERAL("%.4d%.2d%.2d-%.2d%.2d%.2d_"),
229                   create_time_exploded.year,
230                   create_time_exploded.month,
231                   create_time_exploded.day_of_month,
232                   create_time_exploded.hour,
233                   create_time_exploded.minute,
234                   create_time_exploded.second);
235  if (!base::CreateTemporaryDirInDir(directory, job_prefix, &directory)) {
236    LOG(WARNING) << "Cannot create directory for " << job_prefix;
237    return false;
238  }
239
240  int written = base::WriteFile(directory.AppendASCII("ticket.xml"),
241                                ticket.data(),
242                                static_cast<int>(ticket.size()));
243  if (static_cast<size_t>(written) != ticket.size()) {
244    LOG(WARNING) << "Cannot save ticket.";
245    return false;
246  }
247
248  written = base::WriteFile(
249      directory.AppendASCII("data." + file_extension),
250      content.data(), static_cast<int>(content.size()));
251  if (static_cast<size_t>(written) != content.size()) {
252    LOG(WARNING) << "Cannot save data.";
253    return false;
254  }
255
256  VLOG(0) << "Job saved at " << directory.value();
257  return true;
258}
259
260void PrintJobHandler::SetJobState(const std::string& id,
261                                  LocalPrintJob::State state) {
262  DCHECK(!drafts.count(id)) << "Draft should be completed at first";
263
264  std::map<std::string, LocalPrintJobExtended>::iterator job = jobs.find(id);
265  DCHECK(job != jobs.end());
266  job->second.state = state;
267  switch (state) {
268    case LocalPrintJob::STATE_DRAFT:
269      NOTREACHED();
270      break;
271    case LocalPrintJob::STATE_ABORTED:
272    case LocalPrintJob::STATE_DONE:
273      job->second.expiration =
274          base::Time::Now() +
275          base::TimeDelta::FromSeconds(kLocalPrintJobExpirationSec);
276      base::MessageLoop::current()->PostDelayedTask(
277          FROM_HERE,
278          base::Bind(&PrintJobHandler::ForgetLocalJob, AsWeakPtr(), id),
279          base::TimeDelta::FromSeconds(kLocalPrintJobExpirationSec));
280      break;
281    default:
282      NOTREACHED();
283  }
284}
285
286void PrintJobHandler::CompleteDraft(const std::string& id,
287                                    const LocalPrintJob& job) {
288  std::map<std::string, LocalPrintJobDraft>::iterator draft = drafts.find(id);
289  if (draft != drafts.end()) {
290    jobs[id] = LocalPrintJobExtended(job, draft->second.ticket);
291    drafts.erase(draft);
292  }
293}
294
295// TODO(maksymb): Use base::Time for expiration
296base::TimeDelta PrintJobHandler::GetJobExpiration(const std::string& id) const {
297  DCHECK(jobs.count(id));
298  base::Time expiration = jobs.at(id).expiration;
299  if (expiration.is_null())
300    return base::TimeDelta::FromSeconds(kLocalPrintJobExpirationSec);
301  return expiration - base::Time::Now();
302}
303
304void PrintJobHandler::ForgetDraft(const std::string& id) {
305  drafts.erase(id);
306}
307
308void PrintJobHandler::ForgetLocalJob(const std::string& id) {
309  jobs.erase(id);
310}
311