1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.net.tftp;
19
20 import java.io.BufferedInputStream;
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.OutputStream;
29 import java.io.PrintStream;
30 import java.net.SocketTimeoutException;
31 import java.util.HashSet;
32 import java.util.Iterator;
33
34 import org.apache.commons.net.io.FromNetASCIIOutputStream;
35 import org.apache.commons.net.io.ToNetASCIIInputStream;
36
37 /**
38 * A fully multi-threaded tftp server. Can handle multiple clients at the same time. Implements RFC
39 * 1350 and wrapping block numbers for large file support.
40 *
41 * To launch, just create an instance of the class. An IOException will be thrown if the server
42 * fails to start for reasons such as port in use, port denied, etc.
43 *
44 * To stop, use the shutdown method.
45 *
46 * To check to see if the server is still running (or if it stopped because of an error), call the
47 * isRunning() method.
48 *
49 * By default, events are not logged to stdout/stderr. This can be changed with the
50 * setLog and setLogError methods.
51 *
52 * <p>
53 * Example usage is below:
54 *
55 * <code>
56 * public static void main(String[] args) throws Exception
57 * {
58 * if (args.length != 1)
59 * {
60 * System.out
61 * .println("You must provide 1 argument - the base path for the server to serve from.");
62 * System.exit(1);
63 * }
64 *
65 * TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), GET_AND_PUT);
66 * ts.setSocketTimeout(2000);
67 *
68 * System.out.println("TFTP Server running. Press enter to stop.");
69 * new InputStreamReader(System.in).read();
70 *
71 * ts.shutdown();
72 * System.out.println("Server shut down.");
73 * System.exit(0);
74 * }
75 *
76 * </code>
77 *
78 *
79 * @author <A HREF="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</A>
80 * @since 2.0
81 */
82
83 public class TFTPServer implements Runnable
84 {
85 private static final int DEFAULT_TFTP_PORT = 69;
86 public static enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT; }
87
88 private HashSet<TFTPTransfer> transfers_ = new HashSet<TFTPTransfer>();
89 private volatile boolean shutdownServer = false;
90 private TFTP serverTftp_;
91 private File serverReadDirectory_;
92 private File serverWriteDirectory_;
93 private int port_;
94 private Exception serverException = null;
95 private ServerMode mode_;
96
97 /* /dev/null output stream (default) */
98 private static final PrintStream nullStream = new PrintStream(
99 new OutputStream() {
100 @Override
101 public void write(int b){}
102 @Override
103 public void write(byte[] b) throws IOException {}
104 }
105 );
106
107 // don't have access to a logger api, so we will log to these streams, which
108 // by default are set to a no-op logger
109 private PrintStream log_;
110 private PrintStream logError_;
111
112 private int maxTimeoutRetries_ = 3;
113 private int socketTimeout_;
114 private Thread serverThread;
115
116
117 /**
118 * Start a TFTP Server on the default port (69). Gets and Puts occur in the specified
119 * directories.
120 *
121 * The server will start in another thread, allowing this constructor to return immediately.
122 *
123 * If a get or a put comes in with a relative path that tries to get outside of the
124 * serverDirectory, then the get or put will be denied.
125 *
126 * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both.
127 * Modes are defined as int constants in this class.
128 *
129 * @param serverReadDirectory directory for GET requests
130 * @param serverWriteDirectory directory for PUT requests
131 * @param mode A value as specified above.
132 * @throws IOException if the server directory is invalid or does not exist.
133 */
134 public TFTPServer(File serverReadDirectory, File serverWriteDirectory, ServerMode mode)
135 throws IOException
136 {
137 this(serverReadDirectory, serverWriteDirectory, DEFAULT_TFTP_PORT, mode, null, null);
138 }
139
140 /**
141 * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory.
142 *
143 * The server will start in another thread, allowing this constructor to return immediately.
144 *
145 * If a get or a put comes in with a relative path that tries to get outside of the
146 * serverDirectory, then the get or put will be denied.
147 *
148 * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both.
149 * Modes are defined as int constants in this class.
150 *
151 * @param serverReadDirectory directory for GET requests
152 * @param serverWriteDirectory directory for PUT requests
153 * @param mode A value as specified above.
154 * @param log Stream to write log message to. If not provided, uses System.out
155 * @param errorLog Stream to write error messages to. If not provided, uses System.err.
156 * @throws IOException if the server directory is invalid or does not exist.
157 */
158 public TFTPServer(File serverReadDirectory, File serverWriteDirectory, int port, ServerMode mode,
159 PrintStream log, PrintStream errorLog) throws IOException
160 {
161 port_ = port;
162 mode_ = mode;
163 log_ = (log == null ? nullStream: log);
164 logError_ = (errorLog == null ? nullStream : errorLog);
165 launch(serverReadDirectory, serverWriteDirectory);
166 }
167
168 /**
169 * Set the max number of retries in response to a timeout. Default 3. Min 0.
170 *
171 * @param retries
172 */
173 public void setMaxTimeoutRetries(int retries)
174 {
175 if (retries < 0)
176 {
177 throw new RuntimeException("Invalid Value");
178 }
179 maxTimeoutRetries_ = retries;
180 }
181
182 /**
183 * Get the current value for maxTimeoutRetries
184 */
185 public int getMaxTimeoutRetries()
186 {
187 return maxTimeoutRetries_;
188 }
189
190 /**
191 * Set the socket timeout in milliseconds used in transfers. Defaults to the value here:
192 * http://commons.apache.org/net/apidocs/org/apache/commons/net/tftp/TFTP.html#DEFAULT_TIMEOUT
193 * (5000 at the time I write this) Min value of 10.
194 */
195 public void setSocketTimeout(int timeout)
196 {
197 if (timeout < 10)
198 {
199 throw new RuntimeException("Invalid Value");
200 }
201 socketTimeout_ = timeout;
202 }
203
204 /**
205 * The current socket timeout used during transfers in milliseconds.
206 */
207 public int getSocketTimeout()
208 {
209 return socketTimeout_;
210 }
211
212 /*
213 * start the server, throw an error if it can't start.
214 */
215 private void launch(File serverReadDirectory, File serverWriteDirectory) throws IOException
216 {
217 log_.println("Starting TFTP Server on port " + port_ + ". Read directory: "
218 + serverReadDirectory + " Write directory: " + serverWriteDirectory
219 + " Server Mode is " + mode_);
220
221 serverReadDirectory_ = serverReadDirectory.getCanonicalFile();
222 if (!serverReadDirectory_.exists() || !serverReadDirectory.isDirectory())
223 {
224 throw new IOException("The server read directory " + serverReadDirectory_
225 + " does not exist");
226 }
227
228 serverWriteDirectory_ = serverWriteDirectory.getCanonicalFile();
229 if (!serverWriteDirectory_.exists() || !serverWriteDirectory.isDirectory())
230 {
231 throw new IOException("The server write directory " + serverWriteDirectory_
232 + " does not exist");
233 }
234
235 serverTftp_ = new TFTP();
236
237 // This is the value used in response to each client.
238 socketTimeout_ = serverTftp_.getDefaultTimeout();
239
240 // we want the server thread to listen forever.
241 serverTftp_.setDefaultTimeout(0);
242
243 serverTftp_.open(port_);
244
245 serverThread = new Thread(this);
246 serverThread.setDaemon(true);
247 serverThread.start();
248 }
249
250 @Override
251 protected void finalize() throws Throwable
252 {
253 shutdown();
254 }
255
256 /**
257 * check if the server thread is still running.
258 *
259 * @return true if running, false if stopped.
260 * @throws Exception throws the exception that stopped the server if the server is stopped from
261 * an exception.
262 */
263 public boolean isRunning() throws Exception
264 {
265 if (shutdownServer && serverException != null)
266 {
267 throw serverException;
268 }
269 return !shutdownServer;
270 }
271
272 public void run()
273 {
274 try
275 {
276 while (!shutdownServer)
277 {
278 TFTPPacket tftpPacket;
279
280 tftpPacket = serverTftp_.receive();
281
282 TFTPTransfer tt = new TFTPTransfer(tftpPacket);
283 synchronized(transfers_)
284 {
285 transfers_.add(tt);
286 }
287
288 Thread thread = new Thread(tt);
289 thread.setDaemon(true);
290 thread.start();
291 }
292 }
293 catch (Exception e)
294 {
295 if (!shutdownServer)
296 {
297 serverException = e;
298 logError_.println("Unexpected Error in TFTP Server - Server shut down! + " + e);
299 }
300 }
301 finally
302 {
303 shutdownServer = true; // set this to true, so the launching thread can check to see if it started.
304 if (serverTftp_ != null && serverTftp_.isOpen())
305 {
306 serverTftp_.close();
307 }
308 }
309 }
310
311 /**
312 * Stop the tftp server (and any currently running transfers) and release all opened network
313 * resources.
314 */
315 public void shutdown()
316 {
317 shutdownServer = true;
318
319 synchronized(transfers_)
320 {
321 Iterator<TFTPTransfer> it = transfers_.iterator();
322 while (it.hasNext())
323 {
324 it.next().shutdown();
325 }
326 }
327
328 try
329 {
330 serverTftp_.close();
331 }
332 catch (RuntimeException e)
333 {
334 // noop
335 }
336
337 try {
338 serverThread.join();
339 } catch (InterruptedException e) {
340 // we've done the best we could, return
341 }
342 }
343
344 /*
345 * An instance of an ongoing transfer.
346 */
347 private class TFTPTransfer implements Runnable
348 {
349 private TFTPPacket tftpPacket_;
350
351 private boolean shutdownTransfer = false;
352
353 TFTP transferTftp_ = null;
354
355 public TFTPTransfer(TFTPPacket tftpPacket)
356 {
357 tftpPacket_ = tftpPacket;
358 }
359
360 public void shutdown()
361 {
362 shutdownTransfer = true;
363 try
364 {
365 transferTftp_.close();
366 }
367 catch (RuntimeException e)
368 {
369 // noop
370 }
371 }
372
373 public void run()
374 {
375 try
376 {
377 transferTftp_ = new TFTP();
378
379 transferTftp_.beginBufferedOps();
380 transferTftp_.setDefaultTimeout(socketTimeout_);
381
382 transferTftp_.open();
383
384 if (tftpPacket_ instanceof TFTPReadRequestPacket)
385 {
386 handleRead(((TFTPReadRequestPacket) tftpPacket_));
387 }
388 else if (tftpPacket_ instanceof TFTPWriteRequestPacket)
389 {
390 handleWrite((TFTPWriteRequestPacket) tftpPacket_);
391 }
392 else
393 {
394 log_.println("Unsupported TFTP request (" + tftpPacket_ + ") - ignored.");
395 }
396 }
397 catch (Exception e)
398 {
399 if (!shutdownTransfer)
400 {
401 logError_
402 .println("Unexpected Error in during TFTP file transfer. Transfer aborted. "
403 + e);
404 }
405 }
406 finally
407 {
408 try
409 {
410 if (transferTftp_ != null && transferTftp_.isOpen())
411 {
412 transferTftp_.endBufferedOps();
413 transferTftp_.close();
414 }
415 }
416 catch (Exception e)
417 {
418 // noop
419 }
420 synchronized(transfers_)
421 {
422 transfers_.remove(this);
423 }
424 }
425 }
426
427 /*
428 * Handle a tftp read request.
429 */
430 private void handleRead(TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException
431 {
432 InputStream is = null;
433 try
434 {
435 if (mode_ == ServerMode.PUT_ONLY)
436 {
437 transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp
438 .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
439 "Read not allowed by server."));
440 return;
441 }
442
443 try
444 {
445 is = new BufferedInputStream(new FileInputStream(buildSafeFile(
446 serverReadDirectory_, trrp.getFilename(), false)));
447 }
448 catch (FileNotFoundException e)
449 {
450 transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp
451 .getPort(), TFTPErrorPacket.FILE_NOT_FOUND, e.getMessage()));
452 return;
453 }
454 catch (Exception e)
455 {
456 transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp
457 .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage()));
458 return;
459 }
460
461 if (trrp.getMode() == TFTP.NETASCII_MODE)
462 {
463 is = new ToNetASCIIInputStream(is);
464 }
465
466 byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH];
467
468 TFTPPacket answer;
469
470 int block = 1;
471 boolean sendNext = true;
472
473 int readLength = TFTPDataPacket.MAX_DATA_LENGTH;
474
475 TFTPDataPacket lastSentData = null;
476
477 // We are reading a file, so when we read less than the
478 // requested bytes, we know that we are at the end of the file.
479 while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && !shutdownTransfer)
480 {
481 if (sendNext)
482 {
483 readLength = is.read(temp);
484 if (readLength == -1)
485 {
486 readLength = 0;
487 }
488
489 lastSentData = new TFTPDataPacket(trrp.getAddress(), trrp.getPort(), block,
490 temp, 0, readLength);
491 transferTftp_.bufferedSend(lastSentData);
492 }
493
494 answer = null;
495
496 int timeoutCount = 0;
497
498 while (!shutdownTransfer
499 && (answer == null || !answer.getAddress().equals(trrp.getAddress()) || answer
500 .getPort() != trrp.getPort()))
501 {
502 // listen for an answer.
503 if (answer != null)
504 {
505 // The answer that we got didn't come from the
506 // expected source, fire back an error, and continue
507 // listening.
508 log_.println("TFTP Server ignoring message from unexpected source.");
509 transferTftp_.bufferedSend(new TFTPErrorPacket(answer.getAddress(),
510 answer.getPort(), TFTPErrorPacket.UNKNOWN_TID,
511 "Unexpected Host or Port"));
512 }
513 try
514 {
515 answer = transferTftp_.bufferedReceive();
516 }
517 catch (SocketTimeoutException e)
518 {
519 if (timeoutCount >= maxTimeoutRetries_)
520 {
521 throw e;
522 }
523 // didn't get an ack for this data. need to resend
524 // it.
525 timeoutCount++;
526 transferTftp_.bufferedSend(lastSentData);
527 continue;
528 }
529 }
530
531 if (answer == null || !(answer instanceof TFTPAckPacket))
532 {
533 if (!shutdownTransfer)
534 {
535 logError_
536 .println("Unexpected response from tftp client during transfer ("
537 + answer + "). Transfer aborted.");
538 }
539 break;
540 }
541 else
542 {
543 // once we get here, we know we have an answer packet
544 // from the correct host.
545 TFTPAckPacket ack = (TFTPAckPacket) answer;
546 if (ack.getBlockNumber() != block)
547 {
548 /*
549 * The origional tftp spec would have called on us to resend the
550 * previous data here, however, that causes the SAS Syndrome.
551 * http://www.faqs.org/rfcs/rfc1123.html section 4.2.3.1 The modified
552 * spec says that we ignore a duplicate ack. If the packet was really
553 * lost, we will time out on receive, and resend the previous data at
554 * that point.
555 */
556 sendNext = false;
557 }
558 else
559 {
560 // send the next block
561 block++;
562 if (block > 65535)
563 {
564 // wrap the block number
565 block = 0;
566 }
567 sendNext = true;
568 }
569 }
570 }
571 }
572 finally
573 {
574 try
575 {
576 if (is != null)
577 {
578 is.close();
579 }
580 }
581 catch (IOException e)
582 {
583 // noop
584 }
585 }
586 }
587
588 /*
589 * handle a tftp write request.
590 */
591 private void handleWrite(TFTPWriteRequestPacket twrp) throws IOException,
592 TFTPPacketException
593 {
594 OutputStream bos = null;
595 try
596 {
597 if (mode_ == ServerMode.GET_ONLY)
598 {
599 transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
600 .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
601 "Write not allowed by server."));
602 return;
603 }
604
605 int lastBlock = 0;
606 String fileName = twrp.getFilename();
607
608 try
609 {
610 File temp = buildSafeFile(serverWriteDirectory_, fileName, true);
611 if (temp.exists())
612 {
613 transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
614 .getPort(), TFTPErrorPacket.FILE_EXISTS, "File already exists"));
615 return;
616 }
617 bos = new BufferedOutputStream(new FileOutputStream(temp));
618
619 if (twrp.getMode() == TFTP.NETASCII_MODE)
620 {
621 bos = new FromNetASCIIOutputStream(bos);
622 }
623 }
624 catch (Exception e)
625 {
626 transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
627 .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage()));
628 return;
629 }
630
631 TFTPAckPacket lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
632 transferTftp_.bufferedSend(lastSentAck);
633
634 while (true)
635 {
636 // get the response - ensure it is from the right place.
637 TFTPPacket dataPacket = null;
638
639 int timeoutCount = 0;
640
641 while (!shutdownTransfer
642 && (dataPacket == null
643 || !dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
644 .getPort() != twrp.getPort()))
645 {
646 // listen for an answer.
647 if (dataPacket != null)
648 {
649 // The data that we got didn't come from the
650 // expected source, fire back an error, and continue
651 // listening.
652 log_.println("TFTP Server ignoring message from unexpected source.");
653 transferTftp_.bufferedSend(new TFTPErrorPacket(dataPacket.getAddress(),
654 dataPacket.getPort(), TFTPErrorPacket.UNKNOWN_TID,
655 "Unexpected Host or Port"));
656 }
657
658 try
659 {
660 dataPacket = transferTftp_.bufferedReceive();
661 }
662 catch (SocketTimeoutException e)
663 {
664 if (timeoutCount >= maxTimeoutRetries_)
665 {
666 throw e;
667 }
668 // It didn't get our ack. Resend it.
669 transferTftp_.bufferedSend(lastSentAck);
670 timeoutCount++;
671 continue;
672 }
673 }
674
675 if (dataPacket != null && dataPacket instanceof TFTPWriteRequestPacket)
676 {
677 // it must have missed our initial ack. Send another.
678 lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
679 transferTftp_.bufferedSend(lastSentAck);
680 }
681 else if (dataPacket == null || !(dataPacket instanceof TFTPDataPacket))
682 {
683 if (!shutdownTransfer)
684 {
685 logError_
686 .println("Unexpected response from tftp client during transfer ("
687 + dataPacket + "). Transfer aborted.");
688 }
689 break;
690 }
691 else
692 {
693 int block = ((TFTPDataPacket) dataPacket).getBlockNumber();
694 byte[] data = ((TFTPDataPacket) dataPacket).getData();
695 int dataLength = ((TFTPDataPacket) dataPacket).getDataLength();
696 int dataOffset = ((TFTPDataPacket) dataPacket).getDataOffset();
697
698 if (block > lastBlock || (lastBlock == 65535 && block == 0))
699 {
700 // it might resend a data block if it missed our ack
701 // - don't rewrite the block.
702 bos.write(data, dataOffset, dataLength);
703 lastBlock = block;
704 }
705
706 lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), block);
707 transferTftp_.bufferedSend(lastSentAck);
708 if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH)
709 {
710 // end of stream signal - The tranfer is complete.
711 bos.close();
712
713 // But my ack may be lost - so listen to see if I
714 // need to resend the ack.
715 for (int i = 0; i < maxTimeoutRetries_; i++)
716 {
717 try
718 {
719 dataPacket = transferTftp_.bufferedReceive();
720 }
721 catch (SocketTimeoutException e)
722 {
723 // this is the expected route - the client
724 // shouldn't be sending any more packets.
725 break;
726 }
727
728 if (dataPacket != null
729 && (!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket
730 .getPort() != twrp.getPort()))
731 {
732 // make sure it was from the right client...
733 transferTftp_
734 .bufferedSend(new TFTPErrorPacket(dataPacket
735 .getAddress(), dataPacket.getPort(),
736 TFTPErrorPacket.UNKNOWN_TID,
737 "Unexpected Host or Port"));
738 }
739 else
740 {
741 // This means they sent us the last
742 // datapacket again, must have missed our
743 // ack. resend it.
744 transferTftp_.bufferedSend(lastSentAck);
745 }
746 }
747
748 // all done.
749 break;
750 }
751 }
752 }
753 }
754 finally
755 {
756 if (bos != null)
757 {
758 bos.close();
759 }
760 }
761 }
762
763 /*
764 * Utility method to make sure that paths provided by tftp clients do not get outside of the
765 * serverRoot directory.
766 */
767 private File buildSafeFile(File serverDirectory, String fileName, boolean createSubDirs)
768 throws IOException
769 {
770 File temp = new File(serverDirectory, fileName);
771 temp = temp.getCanonicalFile();
772
773 if (!isSubdirectoryOf(serverDirectory, temp))
774 {
775 throw new IOException("Cannot access files outside of tftp server root.");
776 }
777
778 // ensure directory exists (if requested)
779 if (createSubDirs)
780 {
781 createDirectory(temp.getParentFile());
782 }
783
784 return temp;
785 }
786
787 /*
788 * recursively create subdirectories
789 */
790 private void createDirectory(File file) throws IOException
791 {
792 File parent = file.getParentFile();
793 if (parent == null)
794 {
795 throw new IOException("Unexpected error creating requested directory");
796 }
797 if (!parent.exists())
798 {
799 // recurse...
800 createDirectory(parent);
801 }
802
803 if (parent.isDirectory())
804 {
805 if (file.isDirectory())
806 {
807 return;
808 }
809 boolean result = file.mkdir();
810 if (!result)
811 {
812 throw new IOException("Couldn't create requested directory");
813 }
814 }
815 else
816 {
817 throw new IOException(
818 "Invalid directory path - file in the way of requested folder");
819 }
820 }
821
822 /*
823 * recursively check to see if one directory is a parent of another.
824 */
825 private boolean isSubdirectoryOf(File parent, File child)
826 {
827 File childsParent = child.getParentFile();
828 if (childsParent == null)
829 {
830 return false;
831 }
832 if (childsParent.equals(parent))
833 {
834 return true;
835 }
836 else
837 {
838 return isSubdirectoryOf(parent, childsParent);
839 }
840 }
841 }
842
843 /**
844 * Set the stream object to log debug / informational messages. By default, this is a no-op
845 *
846 * @param log
847 */
848 public void setLog(PrintStream log)
849 {
850 this.log_ = log;
851 }
852
853 /**
854 * Set the stream object to log error messsages. By default, this is a no-op
855 *
856 * @param logError
857 */
858 public void setLogError(PrintStream logError)
859 {
860 this.logError_ = logError;
861 }
862 }