1 /*
   2  * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug 8153732 8212202 8221263 8221412 8222108
  27  * @requires (os.family == "Windows")
  28  * @summary Windows remote printer changes do not reflect in lookupPrintServices()
  29  * @run main/manual/othervm -Dsun.java2d.print.minRefreshTime=120 RemotePrinterStatusRefresh
  30  */
  31 
  32 import java.awt.BorderLayout;
  33 import java.awt.Color;
  34 import java.awt.Component;
  35 import java.awt.GridLayout;
  36 import java.awt.event.ActionEvent;
  37 import java.awt.event.WindowAdapter;
  38 import java.awt.event.WindowEvent;
  39 import java.util.ArrayList;
  40 import java.util.Collections;
  41 import java.util.List;
  42 import java.util.concurrent.CountDownLatch;
  43 import javax.print.PrintService;
  44 import javax.print.PrintServiceLookup;
  45 import javax.swing.AbstractListModel;
  46 import javax.swing.BorderFactory;
  47 import javax.swing.Box;
  48 import javax.swing.BoxLayout;
  49 import javax.swing.DefaultListCellRenderer;
  50 import javax.swing.GroupLayout;
  51 import javax.swing.JButton;
  52 import javax.swing.JFrame;
  53 import javax.swing.JLabel;
  54 import javax.swing.JList;
  55 import javax.swing.JPanel;
  56 import javax.swing.JScrollPane;
  57 import javax.swing.JTextArea;
  58 import javax.swing.JTextField;
  59 import javax.swing.SwingUtilities;
  60 import javax.swing.Timer;
  61 
  62 import static javax.swing.BorderFactory.createTitledBorder;
  63 
  64 public class RemotePrinterStatusRefresh extends WindowAdapter {
  65 
  66     private static final long DEFAULT_REFRESH_TIME = 240L;
  67     private static final long MINIMAL_REFRESH_TIME = 120L;
  68 
  69     private static final long refreshTime = getRefreshTime();
  70 
  71     private static final long TIMEOUT = refreshTime * 4 + 60;
  72 
  73 
  74     private static final CountDownLatch latch = new CountDownLatch(1);
  75     private static volatile RemotePrinterStatusRefresh test;
  76 
  77     private volatile boolean testResult;
  78     private volatile boolean testTimedOut;
  79 
  80     private final JFrame frame;
  81 
  82     private JButton refreshButton;
  83     private JButton passButton;
  84     private JButton failButton;
  85 
  86     private final ServiceItemListModel beforeList;
  87     private final ServiceItemListModel afterList;
  88 
  89     private JTextField nextRefresh;
  90     private JTextField timeLeft;
  91 
  92     private final Timer timer;
  93     private final long startTime;
  94 
  95 
  96     private static class ServiceItem {
  97         private enum State {
  98             REMOVED, UNCHANGED, ADDED
  99         }
 100 
 101         final String name;
 102         State state;
 103 
 104         private ServiceItem(final String name) {
 105             this.name = name;
 106             state = State.UNCHANGED;
 107         }
 108 
 109         @Override
 110         public String toString() {
 111             return name;
 112         }
 113 
 114         @Override
 115         public boolean equals(Object obj) {
 116             return (obj instanceof ServiceItem)
 117                     && ((ServiceItem) obj).name.equals(name);
 118         }
 119 
 120         @Override
 121         public int hashCode() {
 122             return name.hashCode();
 123         }
 124     }
 125 
 126     private static class ServiceItemListModel extends AbstractListModel<ServiceItem> {
 127         private final List<ServiceItem> list;
 128 
 129         private ServiceItemListModel(List<ServiceItem> list) {
 130             this.list = list;
 131         }
 132 
 133         @Override
 134         public int getSize() {
 135             return list.size();
 136         }
 137 
 138         @Override
 139         public ServiceItem getElementAt(int index) {
 140             return list.get(index);
 141         }
 142 
 143         private void refreshList(List<ServiceItem> newList) {
 144             list.clear();
 145             list.addAll(newList);
 146             fireChanged();
 147         }
 148 
 149         private void fireChanged() {
 150             fireContentsChanged(this, 0, list.size() - 1);
 151         }
 152     }
 153 
 154     private static class ServiceItemListRenderer extends DefaultListCellRenderer {
 155         @Override
 156         public Component getListCellRendererComponent(JList<?> list,
 157                                                       Object value,
 158                                                       int index,
 159                                                       boolean isSelected,
 160                                                       boolean cellHasFocus) {
 161             Component component =
 162                     super.getListCellRendererComponent(list, value, index,
 163                                                        isSelected, cellHasFocus);
 164             switch (((ServiceItem) value).state) {
 165                 case REMOVED:
 166                     component.setBackground(Color.RED);
 167                     component.setForeground(Color.WHITE);
 168                     break;
 169                 case ADDED:
 170                     component.setBackground(Color.GREEN);
 171                     component.setForeground(Color.BLACK);
 172                     break;
 173                 case UNCHANGED:
 174                 default:
 175                     break;
 176             }
 177             return component;
 178         }
 179     }
 180 
 181     private static final String INSTRUCTIONS_TEXT =
 182             "Please follow the steps for this manual test:\n"
 183                     + "Step 0: \"Before\" list is populated with currently "
 184                     +          "configured printers.\n"
 185                     + "Step 1: Add or Remove a network printer using "
 186                     +          "Windows Control Panel.\n"
 187                     + "Step 2: Wait for 2\u20134 minutes after adding or removing.\n"
 188                     + "             \"Next printer refresh in\" gives you a "
 189                     +          "rough estimation on when update will happen.\n"
 190                     + "Step 3: Click Refresh."
 191                     +          "\"After\" list is populated with updated list "
 192                     +          "of printers.\n"
 193                     + "Step 4: Compare the list of printers in \"Before\" and "
 194                     +          "\"After\" lists.\n"
 195                     + "              Added printers are highlighted with "
 196                     +               "green color, removed ones \u2014 with "
 197                     +               "red color.\n"
 198                     + "Step 5: Click Pass if the list of printers is correctly "
 199                     +          "updated.\n"
 200                     + "Step 6: If the list is not updated, wait for another "
 201                     +          "2\u20134 minutes, and then click Refresh again.\n"
 202                     + "Step 7: If the list does not update, click Fail.\n"
 203                     + "\n"
 204                     + "You have to click Refresh to enable Pass and Fail buttons. "
 205                     + "If no button is pressed,\n"
 206                     + "the test will time out. "
 207                     + "Closing the window also fails the test.";
 208 
 209     public static void main(String[] args) throws Exception {
 210         SwingUtilities.invokeAndWait(RemotePrinterStatusRefresh::createUI);
 211 
 212         latch.await();
 213         if (!test.testResult) {
 214             throw new RuntimeException("Test failed"
 215                 + (test.testTimedOut ? " because of time out" : ""));
 216         }
 217     }
 218 
 219     private static long getRefreshTime() {
 220         String refreshTime =
 221                 System.getProperty("sun.java2d.print.minRefreshTime",
 222                                    Long.toString(DEFAULT_REFRESH_TIME));
 223         try {
 224             long value = Long.parseLong(refreshTime);
 225             return value < MINIMAL_REFRESH_TIME ? MINIMAL_REFRESH_TIME : value;
 226         } catch (NumberFormatException e) {
 227             return DEFAULT_REFRESH_TIME;
 228         }
 229     }
 230 
 231     private static void createUI() {
 232         test = new RemotePrinterStatusRefresh();
 233     }
 234 
 235     private RemotePrinterStatusRefresh() {
 236         frame = new JFrame("RemotePrinterStatusRefresh");
 237         frame.addWindowListener(this);
 238 
 239 
 240         JPanel northPanel = new JPanel(new BorderLayout());
 241         northPanel.add(createInfoPanel(), BorderLayout.NORTH);
 242         northPanel.add(createInstructionsPanel(), BorderLayout.SOUTH);
 243 
 244 
 245         beforeList = new ServiceItemListModel(
 246                 Collections.unmodifiableList(collectPrinterList()));
 247         afterList = new ServiceItemListModel(new ArrayList<>());
 248         logList("Before:", beforeList.list);
 249 
 250         JPanel listPanel = new JPanel(new GridLayout(1, 2));
 251         listPanel.setBorder(createTitledBorder("Print Services"));
 252         listPanel.add(createListPanel(beforeList, "Before:", 'b'));
 253         listPanel.add(createListPanel(afterList, "After:", 'a'));
 254 
 255 
 256         JPanel mainPanel = new JPanel(new BorderLayout());
 257         mainPanel.add(northPanel, BorderLayout.NORTH);
 258         mainPanel.add(listPanel, BorderLayout.CENTER);
 259         mainPanel.add(createButtonPanel(), BorderLayout.SOUTH);
 260 
 261 
 262         frame.add(mainPanel);
 263         frame.pack();
 264         refreshButton.requestFocusInWindow();
 265         frame.setVisible(true);
 266 
 267 
 268         timer = new Timer(1000, this::updateTimeLeft);
 269         timer.start();
 270         startTime = System.currentTimeMillis();
 271         updateTimeLeft(null);
 272     }
 273 
 274     private JPanel createInfoPanel() {
 275         JLabel javaLabel = new JLabel("Java version:");
 276         JTextField javaVersion =
 277                 new JTextField(System.getProperty("java.runtime.version"));
 278         javaVersion.setEditable(false);
 279         javaLabel.setLabelFor(javaVersion);
 280 
 281         JLabel refreshTimeLabel = new JLabel("Refresh interval:");
 282         long minutes = refreshTime / 60;
 283         long seconds = refreshTime % 60;
 284         String interval = String.format("%1$d seconds%2$s",
 285                 refreshTime,
 286                 minutes > 0
 287                     ? String.format(" (%1$d %2$s%3$s)",
 288                         minutes,
 289                         minutes > 1 ? "minutes" : "minute",
 290                         seconds > 0
 291                             ? String.format(" %1$d %2$s",
 292                                 seconds,
 293                                 seconds > 1 ? "seconds" : "second")
 294                             : "")
 295                     : ""
 296         );
 297         JTextField refreshInterval = new JTextField(interval);
 298         refreshInterval.setEditable(false);
 299         refreshTimeLabel.setLabelFor(refreshInterval);
 300 
 301         JLabel nextRefreshLabel = new JLabel("Next printer refresh in:");
 302         nextRefresh = new JTextField();
 303         nextRefresh.setEditable(false);
 304         nextRefreshLabel.setLabelFor(nextRefresh);
 305 
 306         JLabel timeoutLabel = new JLabel("Time left:");
 307         timeLeft = new JTextField();
 308         timeLeft.setEditable(false);
 309         timeoutLabel.setLabelFor(timeLeft);
 310 
 311         JPanel infoPanel = new JPanel();
 312         GroupLayout layout = new GroupLayout(infoPanel);
 313         infoPanel.setLayout(layout);
 314         infoPanel.setBorder(BorderFactory.createTitledBorder("Info"));
 315         layout.setAutoCreateGaps(true);
 316         layout.setHorizontalGroup(
 317             layout.createSequentialGroup()
 318                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
 319                     .addComponent(javaLabel)
 320                     .addComponent(refreshTimeLabel)
 321                     .addComponent(nextRefreshLabel)
 322                     .addComponent(timeoutLabel)
 323                 )
 324                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING, true)
 325                     .addComponent(javaVersion)
 326                     .addComponent(refreshInterval)
 327                     .addComponent(nextRefresh)
 328                     .addComponent(timeLeft)
 329                 )
 330         );
 331         layout.setVerticalGroup(
 332             layout.createSequentialGroup()
 333                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
 334                     .addComponent(javaLabel)
 335                     .addComponent(javaVersion)
 336                 )
 337                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
 338                     .addComponent(refreshTimeLabel)
 339                     .addComponent(refreshInterval))
 340                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
 341                     .addComponent(nextRefreshLabel)
 342                     .addComponent(nextRefresh))
 343                 .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
 344                     .addComponent(timeoutLabel)
 345                     .addComponent(timeLeft))
 346         );
 347         return infoPanel;
 348     }
 349 
 350     private JPanel createInstructionsPanel() {
 351         JPanel instructionsPanel = new JPanel(new BorderLayout());
 352         JTextArea instructionText = new JTextArea(INSTRUCTIONS_TEXT);
 353         instructionText.setEditable(false);
 354         instructionsPanel.setBorder(createTitledBorder("Test Instructions"));
 355         instructionsPanel.add(new JScrollPane(instructionText));
 356         return  instructionsPanel;
 357     }
 358 
 359     private JPanel createListPanel(final ServiceItemListModel model,
 360                                    final String title,
 361                                    final char mnemonic) {
 362         JPanel panel = new JPanel();
 363         panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
 364         JList<ServiceItem> list = new JList<>(model);
 365         list.setCellRenderer(new ServiceItemListRenderer());
 366 
 367         JLabel label = new JLabel(title);
 368         label.setLabelFor(list);
 369         label.setDisplayedMnemonic(mnemonic);
 370         JPanel labelPanel = new JPanel();
 371         labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.X_AXIS));
 372         labelPanel.add(label, BorderLayout.EAST);
 373         labelPanel.add(Box.createHorizontalGlue());
 374 
 375         panel.add(labelPanel);
 376         panel.add(new JScrollPane(list));
 377         return panel;
 378     }
 379 
 380     private JPanel createButtonPanel() {
 381         refreshButton = new JButton("Refresh");
 382         refreshButton.addActionListener(this::refresh);
 383 
 384         passButton = new JButton("Pass");
 385         passButton.addActionListener(this::pass);
 386         passButton.setEnabled(false);
 387 
 388         failButton = new JButton("Fail");
 389         failButton.addActionListener(this::fail);
 390         failButton.setEnabled(false);
 391 
 392         JPanel buttonPanel = new JPanel();
 393         buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
 394         buttonPanel.add(Box.createHorizontalGlue());
 395         buttonPanel.add(refreshButton);
 396         buttonPanel.add(passButton);
 397         buttonPanel.add(failButton);
 398         buttonPanel.add(Box.createHorizontalGlue());
 399         return buttonPanel;
 400     }
 401 
 402     private static List<ServiceItem> collectPrinterList() {
 403         PrintService[] printServices = PrintServiceLookup.lookupPrintServices(null, null);
 404         List<ServiceItem> list = new ArrayList<>(printServices.length);
 405         for (PrintService service : printServices) {
 406             list.add(new ServiceItem(service.getName()));
 407         }
 408         return list;
 409     }
 410 
 411     private static void logList(final String title, final List<ServiceItem> list) {
 412         System.out.println(title);
 413         for (ServiceItem item : list) {
 414             System.out.println(item.name);
 415         }
 416         System.out.println();
 417     }
 418 
 419     private static void compareLists(final ServiceItemListModel before, final ServiceItemListModel after) {
 420         boolean beforeUpdated = false;
 421         boolean afterUpdated = false;
 422 
 423         for (ServiceItem item : before.list) {
 424             if (!after.list.contains(item)) {
 425                 item.state = ServiceItem.State.REMOVED;
 426                 beforeUpdated = true;
 427             } else if (item.state != ServiceItem.State.UNCHANGED) {
 428                 item.state = ServiceItem.State.UNCHANGED;
 429                 beforeUpdated = true;
 430             }
 431         }
 432 
 433         for (ServiceItem item : after.list) {
 434             if (!before.list.contains(item)) {
 435                 item.state = ServiceItem.State.ADDED;
 436                 afterUpdated = true;
 437             } else if (item.state != ServiceItem.State.UNCHANGED) {
 438                 item.state = ServiceItem.State.UNCHANGED;
 439                 afterUpdated = true;
 440             }
 441         }
 442 
 443         if (beforeUpdated) {
 444             before.fireChanged();
 445         }
 446         if (afterUpdated) {
 447             after.fireChanged();
 448         }
 449     }
 450 
 451     @Override
 452     public void windowClosing(WindowEvent e) {
 453         System.out.println("The window closed");
 454         disposeUI();
 455     }
 456 
 457     private void disposeUI() {
 458         timer.stop();
 459         latch.countDown();
 460         frame.dispose();
 461     }
 462 
 463     @SuppressWarnings("unused")
 464     private void refresh(ActionEvent e) {
 465         System.out.println("Refresh button pressed");
 466         afterList.refreshList(collectPrinterList());
 467         compareLists(beforeList, afterList);
 468         passButton.setEnabled(true);
 469         failButton.setEnabled(true);
 470         logList("After:", afterList.list);
 471     }
 472 
 473     @SuppressWarnings("unused")
 474     private void pass(ActionEvent e) {
 475         System.out.println("Pass button pressed");
 476         testResult = true;
 477         disposeUI();
 478     }
 479 
 480     @SuppressWarnings("unused")
 481     private void fail(ActionEvent e) {
 482         System.out.println("Fail button pressed");
 483         testResult = false;
 484         disposeUI();
 485     }
 486 
 487     @SuppressWarnings("unused")
 488     private void updateTimeLeft(ActionEvent e) {
 489         long elapsed = (System.currentTimeMillis() - startTime) / 1000;
 490         long left = TIMEOUT - elapsed;
 491         if (left < 0) {
 492             testTimedOut = true;
 493             disposeUI();
 494         }
 495         timeLeft.setText(formatTime(left));
 496         nextRefresh.setText(formatTime(refreshTime - (elapsed % refreshTime)));
 497     }
 498 
 499     private static String formatTime(final long seconds) {
 500         long minutes = seconds / 60;
 501         return String.format("%d:%02d", minutes, seconds - minutes * 60);
 502     }
 503 
 504 }