001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.ui.apps.scheduler; 029 030import org.opencms.main.CmsLog; 031import org.opencms.scheduler.CmsScheduledJobInfo; 032import org.opencms.ui.CmsCssIcon; 033import org.opencms.ui.CmsVaadinUtils; 034import org.opencms.ui.Messages; 035import org.opencms.ui.apps.A_CmsWorkplaceApp; 036import org.opencms.ui.apps.CmsAppWorkplaceUi; 037import org.opencms.ui.components.CmsConfirmationDialog; 038import org.opencms.ui.components.CmsResourceInfo; 039import org.opencms.ui.components.OpenCmsTheme; 040import org.opencms.ui.contextmenu.CmsContextMenu; 041import org.opencms.ui.contextmenu.CmsMenuItemVisibilityMode; 042import org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry; 043import org.opencms.util.CmsStringUtil; 044 045import java.util.ArrayList; 046import java.util.Collections; 047import java.util.HashSet; 048import java.util.List; 049import java.util.Locale; 050import java.util.Set; 051 052import org.apache.commons.logging.Log; 053 054import com.vaadin.event.MouseEvents; 055import com.vaadin.shared.MouseEventDetails.MouseButton; 056import com.vaadin.ui.themes.ValoTheme; 057import com.vaadin.v7.data.util.BeanItem; 058import com.vaadin.v7.data.util.BeanItemContainer; 059import com.vaadin.v7.event.ItemClickEvent; 060import com.vaadin.v7.event.ItemClickEvent.ItemClickListener; 061import com.vaadin.v7.ui.Table; 062 063/** 064 * Table used to display scheduled jobs, together with buttons for modifying the jobs.<p> 065 * The columns containing the buttons are implemented as generated columns. 066 */ 067public class CmsJobTable extends Table { 068 069 /** 070 * Property columns of table, including their Messages for header.<p> 071 */ 072 protected enum TableProperty { 073 074 /** 075 * Class name column. 076 */ 077 className("className", org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_COL_CLASS_0), 078 079 /** 080 * icon column. 081 */ 082 icon("icon", null), 083 084 /** 085 * last execution date column. 086 */ 087 lastExecution("lastExecution", org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_COL_LASTEXE_0), 088 089 /** 090 * Name column. 091 */ 092 name("name", org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_COL_NAME_0), 093 094 /** 095 * next execution date column. 096 */ 097 nextExecution("nextExecution", org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_COL_NEXTEXE_0); 098 099 /**Message for the header.*/ 100 private String m_header; 101 102 /**Name of column.*/ 103 private String m_name; 104 105 /** 106 * private constructor.<p> 107 * 108 * @param propName name of property 109 * @param header message 110 */ 111 private TableProperty(String propName, String header) { 112 113 m_header = header; 114 m_name = propName; 115 } 116 117 /** 118 * returns property from it's name used for table column ids.<p> 119 * 120 * @param propName to looked up 121 * @return the TableProperty 122 */ 123 static TableProperty get(String propName) { 124 125 for (TableProperty prop : TableProperty.values()) { 126 if (prop.toString().equals(propName)) { 127 return prop; 128 } 129 } 130 return null; 131 } 132 133 /** 134 * Returns all columns with header.<p> 135 * 136 * @return set of TableProperty 137 */ 138 static Set<TableProperty> withHeader() { 139 140 Set<TableProperty> ret = new HashSet<TableProperty>(); 141 142 for (TableProperty prop : TableProperty.values()) { 143 if (prop.getMessageKey() != null) { 144 ret.add(prop); 145 } 146 } 147 return ret; 148 } 149 150 /** 151 * @see java.lang.Enum#toString() 152 */ 153 @Override 154 public String toString() { 155 156 return m_name; 157 } 158 159 /** 160 * Returns the message key.<p> 161 * 162 * @return message 163 */ 164 String getMessageKey() { 165 166 return m_header; 167 } 168 169 } 170 171 /** 172 * Enum representing the actions for which buttons exist in the table rows.<p> 173 */ 174 enum Action { 175 /** Enable / disable. */ 176 activation(org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_ACTION_MACTIVATE_NAME_0, 177 org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_ACTION_MDEACTIVATE_NAME_0), 178 179 /** Create new job from template. */ 180 copy(org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_ACTION_COPY_NAME_0, ""), 181 182 /** Deletes the job. */ 183 delete(org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_ACTION_DELETE_NAME_0, ""), 184 185 /** Edits the job. */ 186 /** Message constant for key in the resource bundle. */ 187 edit(org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_ACTION_EDIT_NAME_0, ""), 188 189 /** Executes the job immediately. */ 190 run(org.opencms.workplace.tools.scheduler.Messages.GUI_JOBS_LIST_ACTION_EXECUTE_NAME_0, ""); 191 192 /** The message key. */ 193 private String m_key; 194 195 /** The message key for activated case.*/ 196 private String m_keyActivated; 197 198 /** 199 * Creates a new action.<p> 200 * 201 * @param key the message key for the action 202 * @param activatedKey an (optional) message key 203 */ 204 private Action(String key, String activatedKey) { 205 206 m_key = key; 207 m_keyActivated = activatedKey; 208 } 209 210 /** 211 * Returns an activated key. 212 * 213 * @return a message key 214 */ 215 String getActivatedMessageKey() { 216 217 return CmsStringUtil.isEmptyOrWhitespaceOnly(m_keyActivated) ? m_key : m_keyActivated; 218 } 219 220 /** 221 * Gets the message key for the action.<p> 222 * 223 * @return the message key 224 */ 225 String getMessageKey() { 226 227 return m_key; 228 } 229 } 230 231 /** 232 * The activate job context menu entry.<p> 233 */ 234 class ActivateEntry implements I_CmsSimpleContextMenuEntry<Set<String>> { 235 236 /** 237 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#executeAction(java.lang.Object) 238 */ 239 public void executeAction(Set<String> data) { 240 241 CmsScheduledJobInfo job = (((Set<CmsJobBean>)getValue()).iterator().next()).getJob(); 242 CmsScheduledJobInfo jobClone = job.clone(); 243 jobClone.setActive(!job.isActive()); 244 245 m_manager.writeElement(jobClone); 246 reloadJobs(); 247 248 } 249 250 /** 251 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getTitle(java.util.Locale) 252 */ 253 public String getTitle(Locale locale) { 254 255 return CmsVaadinUtils.getMessageText(Action.activation.getMessageKey()); 256 } 257 258 /** 259 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getVisibility(java.lang.Object) 260 */ 261 public CmsMenuItemVisibilityMode getVisibility(Set<String> data) { 262 263 if ((data == null) || (data.size() > 1) || (m_manager.getElement(data.iterator().next()) == null)) { 264 return CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 265 } 266 267 @SuppressWarnings("unchecked") 268 CmsScheduledJobInfo job = (((Set<CmsJobBean>)getValue()).iterator().next()).getJob(); 269 270 return !job.isActive() 271 ? CmsMenuItemVisibilityMode.VISIBILITY_ACTIVE 272 : CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 273 } 274 } 275 276 /** 277 * The copy job context menu entry.<p> 278 */ 279 class CopyEntry implements I_CmsSimpleContextMenuEntry<Set<String>> { 280 281 /** 282 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#executeAction(java.lang.Object) 283 */ 284 public void executeAction(Set<String> data) { 285 286 m_manager.openEditDialog(data.iterator().next(), true); 287 } 288 289 /** 290 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getTitle(java.util.Locale) 291 */ 292 public String getTitle(Locale locale) { 293 294 return CmsVaadinUtils.getMessageText(Action.copy.getMessageKey()); 295 } 296 297 /** 298 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getVisibility(java.lang.Object) 299 */ 300 public CmsMenuItemVisibilityMode getVisibility(Set<String> data) { 301 302 return (data != null) && (data.size() == 1) && (m_manager.getElement(data.iterator().next()) != null) 303 ? CmsMenuItemVisibilityMode.VISIBILITY_ACTIVE 304 : CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 305 } 306 } 307 308 /** 309 * The activate job context menu entry.<p> 310 */ 311 class DeActivateEntry implements I_CmsSimpleContextMenuEntry<Set<String>> { 312 313 /** 314 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#executeAction(java.lang.Object) 315 */ 316 public void executeAction(Set<String> data) { 317 318 CmsScheduledJobInfo job = (((Set<CmsJobBean>)getValue()).iterator().next()).getJob(); 319 CmsScheduledJobInfo jobClone = job.clone(); 320 jobClone.setActive(!job.isActive()); 321 322 m_manager.writeElement(jobClone); 323 reloadJobs(); 324 325 } 326 327 /** 328 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getTitle(java.util.Locale) 329 */ 330 public String getTitle(Locale locale) { 331 332 return CmsVaadinUtils.getMessageText(Action.activation.getActivatedMessageKey()); 333 } 334 335 /** 336 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getVisibility(java.lang.Object) 337 */ 338 public CmsMenuItemVisibilityMode getVisibility(Set<String> data) { 339 340 if ((data == null) || (data.size() > 1) || (m_manager.getElement(data.iterator().next()) == null)) { 341 return CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 342 } 343 344 @SuppressWarnings("unchecked") 345 CmsScheduledJobInfo job = (((Set<CmsJobBean>)getValue()).iterator().next()).getJob(); 346 347 return job.isActive() 348 ? CmsMenuItemVisibilityMode.VISIBILITY_ACTIVE 349 : CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 350 } 351 } 352 353 /** 354 * The delete job context menu entry.<p> 355 */ 356 class DeleteEntry implements I_CmsSimpleContextMenuEntry<Set<String>> { 357 358 /** 359 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#executeAction(java.lang.Object) 360 */ 361 @SuppressWarnings("unchecked") 362 public void executeAction(Set<String> data) { 363 364 String jobNames = ""; 365 final List<String> jobIds = new ArrayList<String>(); 366 List<CmsResourceInfo> jobInfos = new ArrayList<CmsResourceInfo>(); 367 for (CmsJobBean job : (Set<CmsJobBean>)getValue()) { 368 jobIds.add(job.getJob().getId()); 369 jobNames += job.getName() + ", "; 370 jobInfos.add(getJobInfo(job.getName(), job.getClassName())); 371 } 372 if (!jobNames.isEmpty()) { 373 jobNames = jobNames.substring(0, jobNames.length() - 2); 374 } 375 376 CmsConfirmationDialog.show( 377 CmsVaadinUtils.getMessageText(Action.delete.getMessageKey()), 378 CmsVaadinUtils.getMessageText(Messages.GUI_SCHEDULER_CONFIRM_DELETE_1, jobNames), 379 new Runnable() { 380 381 public void run() { 382 383 m_manager.deleteElements(jobIds); 384 reloadJobs(); 385 } 386 }).displayResourceInfoDirectly(jobInfos); 387 } 388 389 /** 390 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getTitle(java.util.Locale) 391 */ 392 public String getTitle(Locale locale) { 393 394 return CmsVaadinUtils.getMessageText(Action.delete.getMessageKey()); 395 } 396 397 /** 398 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getVisibility(java.lang.Object) 399 */ 400 public CmsMenuItemVisibilityMode getVisibility(Set<String> data) { 401 402 return (data != null) && (data.size() > 0) && (m_manager.getElement(data.iterator().next()) != null) 403 ? CmsMenuItemVisibilityMode.VISIBILITY_ACTIVE 404 : CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 405 } 406 } 407 408 /** 409 * The edit job context menu entry.<p> 410 */ 411 class EditEntry implements I_CmsSimpleContextMenuEntry<Set<String>>, I_CmsSimpleContextMenuEntry.I_HasCssStyles { 412 413 /** 414 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#executeAction(java.lang.Object) 415 */ 416 public void executeAction(Set<String> data) { 417 418 m_manager.openEditDialog(data.iterator().next(), false); 419 } 420 421 /** 422 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry.I_HasCssStyles#getStyles() 423 */ 424 public String getStyles() { 425 426 return ValoTheme.LABEL_BOLD; 427 } 428 429 /** 430 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getTitle(java.util.Locale) 431 */ 432 public String getTitle(Locale locale) { 433 434 return CmsVaadinUtils.getMessageText(Action.edit.getMessageKey()); 435 } 436 437 /** 438 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getVisibility(java.lang.Object) 439 */ 440 public CmsMenuItemVisibilityMode getVisibility(Set<String> data) { 441 442 return (data != null) && (data.size() == 1) && (m_manager.getElement(data.iterator().next()) != null) 443 ? CmsMenuItemVisibilityMode.VISIBILITY_ACTIVE 444 : CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 445 } 446 } 447 448 /** 449 * The delete job context menu entry.<p> 450 */ 451 class RunEntry implements I_CmsSimpleContextMenuEntry<Set<String>> { 452 453 /** 454 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#executeAction(java.lang.Object) 455 */ 456 public void executeAction(Set<String> data) { 457 458 @SuppressWarnings("unchecked") 459 final CmsScheduledJobInfo job = ((Set<CmsJobBean>)getValue()).iterator().next().getJob(); 460 461 CmsConfirmationDialog.show( 462 CmsVaadinUtils.getMessageText(Action.run.getMessageKey()), 463 CmsVaadinUtils.getMessageText(Messages.GUI_SCHEDULER_CONFIRM_EXECUTE_1, job.getJobName()), 464 new Runnable() { 465 466 public void run() { 467 468 m_manager.runJob(job); 469 } 470 }).displayResourceInfoDirectly( 471 Collections.singletonList(getJobInfo(job.getJobName(), job.getClassName()))); 472 } 473 474 /** 475 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getTitle(java.util.Locale) 476 */ 477 public String getTitle(Locale locale) { 478 479 return CmsVaadinUtils.getMessageText(Action.run.getMessageKey()); 480 } 481 482 /** 483 * @see org.opencms.ui.contextmenu.I_CmsSimpleContextMenuEntry#getVisibility(java.lang.Object) 484 */ 485 public CmsMenuItemVisibilityMode getVisibility(Set<String> data) { 486 487 return (data != null) && (data.size() == 1) && (m_manager.getElement(data.iterator().next()) != null) 488 ? CmsMenuItemVisibilityMode.VISIBILITY_ACTIVE 489 : CmsMenuItemVisibilityMode.VISIBILITY_INVISIBLE; 490 } 491 } 492 493 /** Logger instance for this class. */ 494 static final Log LOG = CmsLog.getLog(CmsJobTable.class); 495 496 /** Serial version id. */ 497 private static final long serialVersionUID = 1L; 498 499 /** The job manager instance. */ 500 public CmsJobManagerApp m_manager; 501 502 /** Bean container for the table. */ 503 protected BeanItemContainer<CmsJobBean> m_beanContainer; 504 505 /** The context menu. */ 506 private CmsContextMenu m_menu; 507 508 /** The available menu entries. */ 509 private List<I_CmsSimpleContextMenuEntry<Set<String>>> m_menuEntries; 510 511 /** 512 * Creates a new instance.<p> 513 * 514 * @param manager the job manager instance 515 */ 516 public CmsJobTable(CmsJobManagerApp manager) { 517 518 m_manager = manager; 519 m_beanContainer = new BeanItemContainer<CmsJobBean>(CmsJobBean.class); 520 setContainerDataSource(m_beanContainer); 521 setVisibleColumns( 522 TableProperty.className.toString(), 523 TableProperty.name.toString(), 524 TableProperty.lastExecution.toString(), 525 TableProperty.nextExecution.toString()); 526 setItemIconPropertyId(TableProperty.icon.toString()); 527 setRowHeaderMode(RowHeaderMode.ICON_ONLY); 528 setColumnWidth(null, 40); 529 530 for (TableProperty prop : TableProperty.withHeader()) { 531 setColumnExpandRatio(prop.toString(), 1); 532 setColumnHeader(prop.toString(), CmsVaadinUtils.getMessageText(prop.getMessageKey())); 533 } 534 setSortContainerPropertyId(TableProperty.name.toString()); 535 getVisibleColumns(); 536 setSelectable(true); 537 setMultiSelect(true); 538 addItemClickListener(new ItemClickListener() { 539 540 private static final long serialVersionUID = -4738296706762013443L; 541 542 public void itemClick(ItemClickEvent event) { 543 544 onItemClick(event, event.getItemId(), event.getPropertyId()); 545 } 546 }); 547 548 m_menu = new CmsContextMenu(); 549 m_menu.setAsTableContextMenu(this); 550 551 setCellStyleGenerator(new CellStyleGenerator() { 552 553 private static final long serialVersionUID = 1L; 554 555 public String getStyle(Table source, Object itemId, Object propertyId) { 556 557 if (TableProperty.className.toString().equals(propertyId)) { 558 return " " + OpenCmsTheme.HOVER_COLUMN; 559 } 560 @SuppressWarnings("unchecked") 561 CmsScheduledJobInfo job = ((BeanItem<CmsJobBean>)source.getItem(itemId)).getBean().getJob(); 562 if (TableProperty.name.toString().equals(propertyId) & job.isActive()) { 563 return " " + OpenCmsTheme.IN_NAVIGATION; 564 } 565 return null; 566 } 567 }); 568 569 } 570 571 /** 572 * Returns the resource info box to the given job.<p> 573 * 574 * @param name the job name 575 * @param className the job class 576 * 577 * @return the info box component 578 */ 579 public static CmsResourceInfo getJobInfo(String name, String className) { 580 581 return new CmsResourceInfo(name, className, new CmsCssIcon(OpenCmsTheme.ICON_JOB)); 582 } 583 584 /** 585 * Returns the available menu entries.<p> 586 * 587 * @return the menu entries 588 */ 589 public List<I_CmsSimpleContextMenuEntry<Set<String>>> getMenuEntries() { 590 591 if (m_menuEntries == null) { 592 m_menuEntries = new ArrayList<I_CmsSimpleContextMenuEntry<Set<String>>>(); 593 m_menuEntries.add(new EditEntry()); 594 m_menuEntries.add(new ActivateEntry()); 595 m_menuEntries.add(new DeActivateEntry()); 596 m_menuEntries.add(new CopyEntry()); 597 m_menuEntries.add(new DeleteEntry()); 598 m_menuEntries.add(new RunEntry()); 599 } 600 return m_menuEntries; 601 } 602 603 /** 604 * Reloads the job table data.<p> 605 */ 606 public void reloadJobs() { 607 608 m_beanContainer.removeAllItems(); 609 for (CmsScheduledJobInfo job : m_manager.getAllElements()) { 610 m_beanContainer.addBean(new CmsJobBean(job)); 611 } 612 sort(); 613 refreshRowCache(); 614 615 } 616 617 /** 618 * Sets the menu entries.<p> 619 * 620 * @param newEntries to be set 621 */ 622 public void setMenuEntries(List<I_CmsSimpleContextMenuEntry<Set<String>>> newEntries) { 623 624 m_menuEntries = newEntries; 625 } 626 627 /** 628 * Calls the edit formular to edit a job.<p> 629 * 630 * @param jobId to be edited. 631 */ 632 void editJob(String jobId) { 633 634 String stateEdit = CmsJobManagerApp.PATH_NAME_EDIT; 635 stateEdit = A_CmsWorkplaceApp.addParamToState(stateEdit, CmsJobManagerApp.PARAM_JOB_ID, jobId); 636 CmsAppWorkplaceUi.get().showApp(CmsScheduledJobsAppConfig.APP_ID, stateEdit); 637 } 638 639 /** 640 * Handles the table item clicks, including clicks on images inside of a table item.<p> 641 * 642 * @param event the click event 643 * @param itemId of the clicked row 644 * @param propertyId column id 645 */ 646 @SuppressWarnings("unchecked") 647 void onItemClick(MouseEvents.ClickEvent event, Object itemId, Object propertyId) { 648 649 if (!event.isCtrlKey() && !event.isShiftKey()) { 650 changeValueIfNotMultiSelect(itemId); 651 // don't interfere with multi-selection using control key 652 if (event.getButton().equals(MouseButton.RIGHT) || (propertyId == null)) { 653 Set<String> jobIds = new HashSet<String>(); 654 for (CmsJobBean job : (Set<CmsJobBean>)getValue()) { 655 jobIds.add(job.getJob().getId()); 656 } 657 m_menu.setEntries(getMenuEntries(), jobIds); 658 m_menu.openForTable(event, itemId, propertyId, this); 659 } else if (event.getButton().equals(MouseButton.LEFT) 660 && TableProperty.className.toString().equals(propertyId)) { 661 662 String jobId = ((Set<CmsJobBean>)getValue()).iterator().next().getJob().getId(); 663 m_manager.defaultAction(jobId); 664 } 665 } 666 } 667 668 /** 669 * Checks value of table and sets it new if needed:<p> 670 * if multiselect: new itemId is in current Value? -> no change of value<p> 671 * no multiselect and multiselect, but new item not selected before: set value to new item<p> 672 * 673 * @param itemId if of clicked item 674 */ 675 private void changeValueIfNotMultiSelect(Object itemId) { 676 677 @SuppressWarnings("unchecked") 678 Set<String> value = (Set<String>)getValue(); 679 if (value == null) { 680 select(itemId); 681 } else if (!value.contains(itemId)) { 682 setValue(null); 683 select(itemId); 684 } 685 } 686 687}