1   /**
2    * JHylaFax - A java client for HylaFAX.
3    *
4    * Copyright (C) 2005 by Steffen Pingel <steffenp@gmx.de>
5    *
6    * This program is free software; you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation; either version 2 of the License, or
9    * (at your option) any later version.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with this program; if not, write to the Free Software
18   * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19   */
20  package net.sf.jhylafax.fax;
21  
22  import gnu.hylafax.Job;
23  import gnu.inet.ftp.ServerResponseException;
24  import java.io.BufferedReader;
25  import java.io.FileInputStream;
26  import java.io.IOException;
27  import java.io.InputStreamReader;
28  import java.text.DateFormat;
29  import java.text.ParseException;
30  import java.text.SimpleDateFormat;
31  import java.util.Date;
32  import java.util.Locale;
33  import java.util.NoSuchElementException;
34  import java.util.StringTokenizer;
35  import java.util.TimeZone;
36  import net.sf.jhylafax.Settings;
37  import net.sf.jhylafax.fax.FaxJob.JobType;
38  import net.sf.jhylafax.fax.FaxJob.PageChopping;
39  import net.sf.jhylafax.fax.Modem.Volume;
40  import org.apache.commons.logging.Log;
41  import org.apache.commons.logging.LogFactory;
42  
43  /**
44   * Provides static methods to handle the server communication.
45   * 
46   * @author Steffen Pingel
47   */
48  public class HylaFAXClientHelper extends Thread {
49  	
50  	/**
51  	 * The format used for docq, contains all valid tokens except %p.
52  	 * 
53  	 * <p>Note: %a, %c, %m adds a line break, therefore it needs to be last.
54  	 */
55  	public final static String FILEFMT = "__FILEFMT |%d |%f |%g |%i |%l |%o |%p |%q |%r |%s |%m ";
56  	
57  	/**
58  	 * The format used for sendq and doneq, contains all valid tokens except %T,
59  	 * %Z, %z.
60  	 * 
61  	 * <p>Note: %s may contain line breaks, therefore it is last
62  	 */
63  	public final static String JOBFMT = "__JOBFMT |%a |%b |%c |%e |%f |%g |%h |%i |%j |%k |%l |%m |%n |%o |%p |%q |%r |%t |%u |%v |%w |%x |%y |%z"
64  		+ " |%A |%B |%C |%D |%E |%F |%G |%H |%I |%J |%K |%L |%M |%N |%O |%P |%Q |%R |%S |%U |%V |%W |%X |%Y |%Z |%s ";
65  	
66  	private final static Log logger = LogFactory.getLog(HylaFAXClientHelper.class);
67  	
68  	/**
69  	 * The format used for modem status, contains all valid tokens.
70  	 */
71  	public final static String MODEMFMT = "__MODEMFMT |%h |%l |%m |%n |%r |%s |%t |%v |%z ";
72  	
73  	/**
74  	 * The format strings need an additional space to avoid empty tokens.
75  	 */
76  	protected final static String QUEUE_SEPARATOR = "|";
77  	
78  	/**
79  	 * The format used for recvq, contains all valid tokens except %m, %t.
80       * 
81       * <p>%q has been removed as well since it causes some versions of HylaFAX 
82       * to segfault (#1496477).
83  	 */
84  	//public final static String RCVFMT = "__RCVFMT |%Y |%a |%b |%d |%e |%f |%h |%i |%j |%l |%n |%o |%p |%q |%r |%s |%w |%z ";
85      public final static String RCVFMT = "__RCVFMT |%Y |%a |%b |%d |%e |%f |%h |%i |%j |%l |%n |%o |%p |%r |%s |%w |%z ";
86      
87  	private static DateFormat fileDateFormat = new SimpleDateFormat("MMM dd HH:mm:ss yyyy", Locale.ENGLISH);
88  
89  	public static void applyParameter(Job faxJob, FaxJob job) throws ServerResponseException, IOException
90  	{
91  		//faxJob.setChopThreshold(3);
92  		faxJob.setDialstring(job.getNumber());
93  		if (job.getSender() != null && job.getSender().trim().length() > 0) {
94  			faxJob.setFromUser(job.getSender());
95  		}
96  		faxJob.setKilltime("000259");
97  		faxJob.setMaximumDials(job.getMaxDials());
98  		faxJob.setMaximumTries(job.getMaxDials());
99  		if (job.getNotifyAdress() != null && job.getNotifyAdress().trim().length() > 0) {
100 			faxJob.setNotifyAddress(job.getNotifyAdress());
101 		}
102 		if (job.getNotify() != null) {
103 			faxJob.setNotifyType(job.getNotify());
104 		}
105 		faxJob.setPageChop(Job.CHOP_DEFAULT);
106 		faxJob.setPageWidth(job.getPageWidth());
107 		faxJob.setPageLength(job.getPageLength());
108 		faxJob.setPriority(job.getPriority());
109 		faxJob.setProperty("SENDTIME", calculateTime(job.getSendTime(), Settings.TIMEZONE.getValue()));
110 		faxJob.setVerticalResolution(job.getVerticalResolution());
111 	}
112 	
113 	public static String calculateTime(Date sendTime, String timeZoneID) {
114 		if (sendTime == null) {
115 			return "NOW";
116 		}
117 		else {
118 			long date = sendTime.getTime();
119 			
120 			TimeZone tz = TimeZone.getTimeZone(timeZoneID);
121 //			tz.setStartRule(Calendar.MARCH, -1, Calendar.SUNDAY,  2*60*60*1000);
122 //			tz.setEndRule(Calendar.OCTOBER, -1, Calendar.SUNDAY,  2*60*60*1000);
123 
124 			date -= tz.getRawOffset();
125 			if (tz.inDaylightTime(sendTime)) {
126 				date -= 3600 * 1000;
127 			}
128 
129 			return new SimpleDateFormat("yyyyMMddHHmm").format(new Date(date));
130 		}
131 	}
132 
133 	private static JobType getJobType(char c)
134 	{
135 		switch (c) {
136 		case 'P':
137 			return JobType.PAGER;
138 		default: // 'F'
139 			return JobType.FACSIMILE;
140 		}
141 	}
142 
143 	private final static String getNotify(char notify) {
144 		switch (notify) {
145 		case 'D' :
146 			return Job.NOTIFY_DONE;
147 		case 'Q' :
148 			return Job.NOTIFY_REQUEUE;
149 		case 'A' :
150 			return Job.NOTIFY_ALL;
151 		default :
152 			return Job.NOTIFY_NONE;
153 		}
154 	}
155 
156 	private static PageChopping getPageChopping(char c)
157 	{
158 		switch (c) {
159 		case 'D':
160 			return PageChopping.DISABLED;
161 		case 'A':
162 			return PageChopping.ALL;
163 		case 'L':
164 			return PageChopping.LAST;
165 		default: // ' '
166 			return PageChopping.DEFAULT;
167 		}
168 	}
169 
170 	public final static FaxJob.State getState(char state) {
171 		switch (state) {
172 		case 'T' : 
173 			return FaxJob.State.SUSPENDED;
174 		case 'P' : 
175 			return FaxJob.State.PENDING;
176 		case 'S' : 
177 			return FaxJob.State.SLEEPING;
178 		case 'B' : 
179 			return FaxJob.State.BLOCKED;
180 		case 'W' : 
181 			return FaxJob.State.WAITING;
182 		case 'D' : 
183 			return FaxJob.State.DONE;
184 		case 'R' : 
185 			return FaxJob.State.RUNNING;
186 		case 'F' : 
187 			return FaxJob.State.FAILED;
188 		default : // '?'
189 			return FaxJob.State.UNDEFINED;
190 		}		
191 	}
192 	
193 	private static Volume getVolume(char c)
194 	{
195 		// TODO add switch
196 		return Volume.OFF;
197 	}
198 	
199 	public final static void initializeFromSettings(FaxJob job) {
200 		job.setSender(Settings.FULLNAME.getValue());
201 		job.setNotifyAdress(Settings.EMAIL.getValue());
202 		job.setMaxDials(Settings.MAXDIALS.getValue());
203 		job.setMaxTries(Settings.MAXTRIES.getValue());
204 		job.setNotify(Settings.NOTIFICATION.getValue().getCommand());
205 		job.setPageLength(Settings.PAPER.getValue().getHeight());
206 		job.setPageWidth(Settings.PAPER.getValue().getWidth());
207 		job.setPriority(Settings.PRIORITY.getValue());
208 		job.setResolution(Settings.RESOLUTION.getValue().getLinesPerInch());
209 	}
210 
211 	public static boolean isPostscript(String filename) throws IOException {
212 		BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(filename)));
213 		try {
214 			return in.readLine().startsWith("%!");
215 		}
216 		finally {
217 			try {
218 				in.close();
219 			} catch (IOException e) {
220 			}
221 		}
222 	}
223 
224 	static int parseDuration(String s)
225 	{
226 		StringTokenizer t = new StringTokenizer(s, ":");
227 		int duration = 0;
228 		while (t.hasMoreTokens()) {
229 			int n = Integer.parseInt(t.nextToken());
230 			duration = duration * 60 + n;
231 		}
232 		return duration;
233 	}
234 
235 	public final static Document parseFileFmt(String response) {
236 		StringTokenizer st = new StringTokenizer(response, QUEUE_SEPARATOR);
237 		StringTokenizer jf = new StringTokenizer(FILEFMT, QUEUE_SEPARATOR);
238 		Document file = new Document();
239 		while (st.hasMoreElements() && jf.hasMoreElements()) {
240 			char c = jf.nextToken().charAt(1);
241 			String s = st.nextToken().trim();
242 			if (s.length() > 0) {
243 				try {
244 					// parse
245 					switch (c) {
246 					case 'a':
247 						file.setLastAccessTime(fileDateFormat.parse(s));
248 						break;
249 					case 'c':
250 						file.setCreationTime(fileDateFormat.parse(s));
251 						break;
252 					case 'd':
253 						file.setDeviceNumber(Integer.parseInt(s, 8));
254 						break;
255 					case 'f':
256 						file.setFilename(s);
257 						break;
258 					case 'g':
259 						file.setGroupID(Integer.parseInt(s));
260 						break;
261 					case 'i':
262 						file.setInodeNumber(Long.parseLong(s));
263 						break;
264 					case 'l':
265 						file.setLinkCount(Integer.parseInt(s));
266 						break;
267 					case 'm':
268 						file.setLastModificationTime(fileDateFormat.parse(s));
269 						break;
270 					case 'o':
271 						file.setOwner(s);
272 						break;
273 					case 'p': // Fax-style protection flags (no group bits)
274 						// 'q' is used instead
275 						break;
276 					case 'q':
277 						file.setPermissions(s);
278 						break;
279 					case 'r':
280 						file.setRootDeviceNumber(Integer.parseInt(s));
281 						break;
282 					case 's':
283 						file.setFilesize(Long.parseLong(s));
284 						break;
285 					case 'u':
286 						file.setOwnerID(Integer.parseInt(s));
287 						break;
288 					}
289 				}
290 				catch (NumberFormatException e) {
291 					logger.info("Error parsing respone", e);
292 				}
293 				catch (ParseException e) {
294 					logger.info("Error parsing response", e);
295 				}
296 			}
297 		}
298 		return file;
299 	}
300 	
301 	public final static Object parseFmt(String response) {
302 		if (logger.isDebugEnabled()) logger.debug("Received: " + response);
303 		
304 		if (response.trim().length() == 0) {
305 			// work around a bug in HylaFax
306 			return null;
307 		}
308 		if (response.startsWith("__JOBFMT")) {
309 			return parseJobFmt(response);
310 		}
311 		else if (response.startsWith("__RCVFMT")) {
312 			return parseRcvFmt(response);
313 		}
314 		else if (response.startsWith("__FILEFMT")) {
315 			return parseFileFmt(response);
316 		}
317 		else if (response.startsWith("__MODEMFMT")) {
318 			return parseModemFmt(response);
319 		}
320 		else {
321 			logger.error("Invalid response: " + response);
322 			return null;
323 		}
324 	}
325 
326 	public final static FaxJob parseJobFmt(String response) {
327 		StringTokenizer st = new StringTokenizer(response, QUEUE_SEPARATOR);
328 		StringTokenizer jf = new StringTokenizer(JOBFMT, QUEUE_SEPARATOR);
329 		FaxJob job = new FaxJob();
330 		while (st.hasMoreElements() && jf.hasMoreElements()) {
331 			char c = jf.nextToken().charAt(1);
332 			String s = st.nextToken().trim();
333 			if (s.length() > 0) {
334 				try {
335 					switch (c) {
336 					case 'a' :
337 						job.setState(getState(s.charAt(0)));
338 						break;
339 					case 'b':
340 						job.setConsecutiveFailedTries(Integer.parseInt(s));
341 						break;
342 					case 'c':
343 						job.setClientMachineName(s);
344 						break;
345 					case 'd' :
346 						job.setDialsAttempted(Integer.parseInt(s));
347 						break;
348 					case 'e' :
349 						job.setNumber(s);
350 						break;
351 					case 'f':
352 						job.setConsecutiveFailedDials(Integer.parseInt(s));
353 						break;
354 					case 'g':
355 						job.setGroupID(Integer.parseInt(s));
356 						break;
357 					case 'h':
358 						job.setPageChopping(getPageChopping(s.charAt(0)));
359 						break;
360 					case 'i' :
361 						job.setPriority((new Integer(s)).intValue());
362 						break;
363 					case 'j' :
364 						job.setID((new Integer(s)).intValue());
365 						break;
366 					case 'k':
367 						job.setKillTime(s);
368 						break;
369 					case 'l' :
370 						// FIXME 'any' job.setPageLength(Integer.parseInt(s));
371 						break;
372 					case 'm':
373 						job.setAssignedModem(s);
374 						break;
375 					case 'n' :
376 						job.setNotify(getNotify(s.charAt(0)));
377 						break;
378 					case 'o' :
379 						job.setOwner(s);
380 						break;
381 					case 'p':
382 						job.setPagesTransmitted(Integer.parseInt(s));
383 						break;
384 					case 'q':
385 						job.setRetryTime(parseDuration(s));
386 						break;
387 					case 'r' :
388 						job.setResolution((new Integer(s)).intValue());
389 						break;
390 					case 's' :
391 						job.setLastError(s);
392 						break;
393 					case 't' :
394 						job.setTriesAttempted((new Integer(s)).intValue());
395 						break;
396 					case 'u' :
397 						job.setMaxTries((new Integer(s)).intValue());
398 						break;
399 					case 'v':
400 						job.setClientDialString(s);
401 						break;
402 					case 'w' :
403 						job.setPageWidth(Integer.parseInt(s));
404 						break;
405 					case 'x' :
406 						// FIXME 'x/y' job.setMaxDials((new Integer(s)).intValue());
407 						break;
408 					case 'z' :
409 						// the handling code never worked correctly, use
410 						// 'Y' instead
411 						//Date date = parseDate(s, true);
412 						//job.setSendTime(date);
413 						break;
414 					case 'A':
415 						job.setDestinationSubAddress(s);
416 						break;
417 					case 'B':
418 						job.setDestinationPassword(s);
419 						break;
420 					case 'C':
421 						job.setDestinationCompanyName(s);
422 						break;
423 					case 'D' : {
424 						StringTokenizer t = new StringTokenizer(s, ":");
425 						job.setDialsAttempted(Integer.parseInt(t.nextToken()));
426 						job.setMaxDials(Integer.parseInt(t.nextToken()));
427 						break; }
428 					case 'E':
429 						job.setDesiredSignallingRate(s);
430 						break;
431 					case 'F':
432 						job.setClientDialString(s);
433 						break;
434 					case 'G':
435 						job.setDesiredMinScanline(s);
436 						break;
437 					case 'H':
438 						job.setDesiredDataFormat(s);
439 						break;
440 					case 'I':
441 						job.setClientSchedulingPriority(s);
442 						break;
443 					case 'J':
444 						job.setClientJobTag(s);
445 						break;
446 					case 'K':
447 						job.setDesiredECM(s);
448 						break;
449 					case 'L':
450 						job.setDestinationLocation(s);
451 						break;
452 					case 'M':
453 						job.setNotifyAdress(s);
454 						break;
455 					case 'N':
456 						job.setUsePrivateTagLine("P".equals(s));
457 						break;
458 					case 'P' : {
459 						StringTokenizer t = new StringTokenizer(s, ":");
460 						job.setPagesTransmitted(Integer.parseInt(t.nextToken()));
461 						job.setPageCount(Integer.parseInt(t.nextToken()));
462 						break; }
463 					case 'R':
464 						job.setReceiver(s);
465 						break;
466 					case 'S':
467 						job.setSender(s);
468 						break;
469 					case 'T': // Total # tries/maximum # tries
470 						// %t, %u are used instead
471 						break;
472 					case 'U':
473 						job.setChoppingThreshold(Double.parseDouble(s));
474 						break;
475 					case 'V':
476 						job.setJobDoneOperation(s);
477 						break;
478 					case 'W':
479 						job.setCommunicationIdentifier(s);
480 						break;
481 					case 'X':
482 						job.setJobType(getJobType(s.charAt(0)));
483 						break;
484 					case 'Y': {
485 						Date date = new SimpleDateFormat("yyyy/MM/dd HH.mm.ss").parse(s); 
486 						job.setSendTime(date);
487 						break; }
488 					case 'Z': {
489 						// should work, but for some reason calculates the
490 						// wrong time, so 'Y' is used instead
491 						Date date = new Date(Long.parseLong(s));
492 						//job.setSendTime(date); 
493 						break; }
494 					}
495 				} 
496 				catch (ParseException e) {
497 					logger.info("Error parsing response", e);
498 				}
499 				catch (NumberFormatException e) {
500 					logger.info("Error parsing response", e);
501 				}
502 				catch (NoSuchElementException e) {
503 					logger.info("Error parsing response", e);
504 				}
505 			}
506 		}
507 		return job;
508 	}
509 
510 	
511 	public final static Modem parseModemFmt(String response) {
512 		StringTokenizer st = new StringTokenizer(response, QUEUE_SEPARATOR);
513 		StringTokenizer jf = new StringTokenizer(MODEMFMT, QUEUE_SEPARATOR);
514 		Modem modem = new Modem();
515 		while (st.hasMoreElements() && jf.hasMoreElements()) {
516 			char c = jf.nextToken().charAt(1);
517 			String s = st.nextToken().trim();
518 			if (s.length() > 0) {
519 				try {
520 					switch (c) {
521 					case 'h':
522 						modem.setHostname(s);
523 						break;
524 					case 'l':
525 						modem.setLocalIdentifier(s);
526 						break;
527 					case 'm':
528 						modem.setCanonicalName(s);
529 						break;
530 					case 'n':
531 						modem.setFaxNumber(s);
532 						break;
533 					case 'r':
534 						modem.setMaxPagesPerCall(Integer.parseInt(s));
535 						break;
536 					case 's':
537 						modem.setStatus(s);
538 						break;
539 					case 't': {
540 						StringTokenizer t = new StringTokenizer(s, ":");
541 						modem.setServerTracing(Integer.parseInt(t.nextToken()));
542 						modem.setSessionTracing(Integer.parseInt(t.nextToken()));
543 						break; }
544 					case 'v':
545 						modem.setSpeakerVolume(getVolume(s.charAt(0)));
546 						break;
547 					case 'z':
548 						modem.setRunning("*".equals(s));
549 						break;
550 					}
551 				}
552 				catch (NumberFormatException e) {
553 					logger.info("Error parsing respone", e);
554 				}
555 				catch (NoSuchElementException e) {
556 					logger.info("Error parsing response", e);
557 				}
558 			}
559 		}
560 		return modem;
561 	}
562 	
563 	public final static ReceivedFax parseRcvFmt(String response) {
564 		StringTokenizer st = new StringTokenizer(response, QUEUE_SEPARATOR);
565 		StringTokenizer jf = new StringTokenizer(RCVFMT, QUEUE_SEPARATOR);
566 		ReceivedFax fax = new ReceivedFax();
567 		while (st.hasMoreElements() && jf.hasMoreElements()) {
568 			char c = jf.nextToken().charAt(1);
569 			String s = st.nextToken().trim();
570 			if (s.length() > 0) {
571 				try {
572 					switch (c) {
573 					case 'Y':
574 						Date date = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss").parse(s); 
575 						fax.setReceivedTime(date);
576 						break;
577 					case 'a' :
578 						fax.setSubAddress(s);
579 						break;
580 					case 'b' :
581 						fax.setSignallingRate(Integer.parseInt(s));
582 						break;
583 					case 'd' :
584 						fax.setDataFormat(s);
585 						break;
586 					case 'e' :
587 						fax.setLastError(s);
588 						break;
589 					case 'f' :
590 						fax.setFilename(s);
591 						break;
592 					case 'h' :
593 						fax.setTimeSpent(parseDuration(s));
594 						break;
595 					case 'i' :
596 						fax.setCallerIDName(s);
597 						break;
598 					case 'j' :
599 						fax.setCallerIDNumber(s);
600 						break;
601 					case 'l' :
602 						// FIXME '4294967295' fax.setPageLength(Integer.parseInt(s));
603 						break;
604 					case 'm' : // Fax-style protection mode string
605 						// 'q' is used instead
606 						break;
607 					case 'n' :
608 						fax.setFilesize(Long.parseLong(s));
609 						break;
610 					case 'o' :
611 						fax.setOwner(s);
612 						break;
613 					case 'p' :
614 						fax.setPageCount(Integer.parseInt(s));
615 						break;
616 					case 'q' :
617 						fax.setProtectionMode(Integer.parseInt(s));
618 						break;
619 					case 'r' :
620 						fax.setResolution(Integer.parseInt(s));
621 						break;
622 					case 's' :
623 						fax.setSender(s);
624 						break;
625 					case 't' :
626 						// the handling code never worked correctly
627 						// 'Y' is used instead
628 						//job.setSendTime(parseDate(s, false));
629 						break;
630 					case 'w' :
631 						fax.setPageWidth(Integer.parseInt(s));
632 						break;
633 					case 'z' :
634 						fax.setReceiving(s.equals("*"));
635 						break;
636 					}
637 				} 
638 				catch (ParseException e) {
639 					logger.info("Error parsing response", e);
640 				}
641 				catch (NumberFormatException e) {
642 					logger.info("Error parsing response", e);
643 				}
644 			}
645 		}
646 		return fax;
647 	}
648 	
649 	public static void setJobProperties(Job faxJob, FaxJob job) throws ServerResponseException, IOException
650 	{
651 		/*
652 		job.setNumber(faxJob.getDialstring());
653 		job.setChopThreshold(faxJob.getChopThreshold());
654 		job.setDialstring(faxJob.getNumber());
655 		job.setSender(faxJob.getSender());
656 		job.setKilltime(faxJob.getKilltime());
657 		job.setMaxDials(faxJob.getMaximumDials());
658 		job.setMaxTries(faxJob.getMaximumTries());
659 		job.setNotifyAddress(faxJob.getNotifyAddress());
660 		job.setNotify(faxJob.getNotifyType());
661 		job.setPageChop(faxJob.getPageChop());
662 		job.setPaperWidth(faxJob.getPageWidth());
663 		job.setPaperHeight(faxJob.getPageLength());
664 		job.setPriority(faxJob.getPriority());
665 		job.setSendTime(faxJob.getRetrytime());
666 		job.setResolution(faxJob.getVerticalResolution());
667 		job.setDocumentNames(faxJob.getDocumentName());
668 		*/
669 	}
670 
671 }