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