001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (C) Alkacon Software (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.gwt.client.seo; 029 030import org.opencms.gwt.client.CmsCoreProvider; 031import org.opencms.gwt.client.rpc.CmsRpcAction; 032import org.opencms.gwt.client.ui.CmsPushButton; 033import org.opencms.gwt.client.ui.I_CmsButton; 034import org.opencms.gwt.client.ui.I_CmsButton.ButtonStyle; 035import org.opencms.gwt.client.ui.css.I_CmsInputCss; 036import org.opencms.gwt.client.ui.css.I_CmsInputLayoutBundle; 037import org.opencms.gwt.client.ui.input.CmsSelectBox; 038import org.opencms.gwt.client.ui.input.CmsTextBox; 039import org.opencms.gwt.client.util.CmsDomUtil; 040import org.opencms.gwt.shared.alias.CmsAliasBean; 041import org.opencms.gwt.shared.alias.CmsAliasMode; 042import org.opencms.util.CmsUUID; 043 044import java.util.ArrayList; 045import java.util.HashMap; 046import java.util.LinkedHashMap; 047import java.util.List; 048import java.util.Map; 049 050import com.google.gwt.dom.client.Style; 051import com.google.gwt.dom.client.Style.FontWeight; 052import com.google.gwt.dom.client.Style.Unit; 053import com.google.gwt.event.dom.client.ClickEvent; 054import com.google.gwt.event.dom.client.ClickHandler; 055import com.google.gwt.event.dom.client.KeyPressEvent; 056import com.google.gwt.event.dom.client.KeyPressHandler; 057import com.google.gwt.event.logical.shared.ValueChangeEvent; 058import com.google.gwt.event.logical.shared.ValueChangeHandler; 059import com.google.gwt.user.client.rpc.AsyncCallback; 060import com.google.gwt.user.client.ui.Composite; 061import com.google.gwt.user.client.ui.FlowPanel; 062import com.google.gwt.user.client.ui.HorizontalPanel; 063import com.google.gwt.user.client.ui.Label; 064import com.google.gwt.user.client.ui.PushButton; 065 066/** 067 * A widget used for editing the alias list of a page.<p> 068 */ 069public class CmsAliasList extends Composite { 070 071 /** 072 * A helper class which encapsulates the input widgets for a single alias.<p> 073 */ 074 protected class AliasControls { 075 076 /** The alias to which these controls belong. */ 077 protected CmsAliasBean m_alias; 078 079 /** A string which uniquely identiy this set of controls. */ 080 protected String m_id = "" + (idCounter++); //$NON-NLS-1$ 081 082 /** The select box for selecting the alias mode. */ 083 protected CmsSelectBox m_selectBox; 084 085 /** The text box for the alias path. */ 086 protected CmsTextBox m_textBox; 087 088 /** 089 * Creates a new alias controls instance.<p> 090 * 091 * @param alias the alias to which the controls belong 092 * @param textBox the text box for entering the alias site path 093 * @param selectBox the select box for selecting alias modes 094 */ 095 public AliasControls(CmsAliasBean alias, CmsTextBox textBox, CmsSelectBox selectBox) { 096 097 m_alias = alias; 098 m_textBox = textBox; 099 m_selectBox = selectBox; 100 } 101 102 /** 103 * Gets the alias to which these controls belong.<p> 104 * 105 * @return the alias to which these controls belong 106 */ 107 public CmsAliasBean getAlias() { 108 109 return m_alias; 110 } 111 112 /** 113 * Gets the id of this set of controls.<p> 114 * 115 * @return the id 116 */ 117 public String getId() { 118 119 return m_id; 120 } 121 122 /** 123 * Gets the alias mode select box.<p> 124 * 125 * @return the alias mode select box 126 */ 127 public CmsSelectBox getSelectBox() { 128 129 return m_selectBox; 130 } 131 132 /** 133 * Gets the text box for the alias site path.<p> 134 * 135 * @return the text box for the alias site path 136 */ 137 public CmsTextBox getTextBox() { 138 139 return m_textBox; 140 } 141 142 } 143 144 /** The CSS bundle for input widgets. */ 145 public static final I_CmsInputCss INPUT_CSS = I_CmsInputLayoutBundle.INSTANCE.inputCss(); 146 147 /** The alias messages. */ 148 protected static CmsAliasMessages aliasMessages = new CmsAliasMessages(); 149 150 /** Static variable used to generate new ids. */ 151 protected static int idCounter; 152 153 /** The callback which is normally used for validation of the site paths. */ 154 protected AsyncCallback<Map<String, String>> m_defaultValidationHandler = new AsyncCallback<Map<String, String>>() { 155 156 public void onFailure(Throwable caught) { 157 158 // do nothing 159 } 160 161 public void onSuccess(java.util.Map<String, String> result) { 162 163 for (Map.Entry<String, String> entry : result.entrySet()) { 164 String id = entry.getKey(); 165 String errorMessage = entry.getValue(); 166 AliasControls controls = m_aliasControls.get(id); 167 controls.getTextBox().setErrorMessage(errorMessage); 168 if (errorMessage != null) { 169 m_hasValidationErrors = true; 170 } 171 } 172 } 173 }; 174 175 /** A flag used to keep track of whether the last validation had any errors. */ 176 protected boolean m_hasValidationErrors; 177 178 /** The structure id of the page for which the aliases are being edited. */ 179 protected CmsUUID m_structureId; 180 181 /** A map containing the alias controls which are currently visible. */ 182 LinkedHashMap<String, AliasControls> m_aliasControls = new LinkedHashMap<String, AliasControls>(); 183 184 /** This panel contains the existing aliases. */ 185 private FlowPanel m_changeBox = new FlowPanel(); 186 187 /** This panel contains input widgets for adding new aliases. */ 188 private FlowPanel m_newBox = new FlowPanel(); 189 190 /** The root panel for this widget. */ 191 private FlowPanel m_panel = new FlowPanel(); 192 193 /** 194 * Creates a new widget instance.<p> 195 * 196 * @param structureId the structure id of the page for which the aliases should be edited 197 * @param aliases the aliases being edited 198 */ 199 public CmsAliasList(CmsUUID structureId, List<CmsAliasBean> aliases) { 200 201 initWidget(m_panel); 202 203 m_panel.addStyleName(INPUT_CSS.highTextBoxes()); 204 m_structureId = structureId; 205 Label newLabel = createLabel(aliasMessages.newAlias()); 206 m_panel.add(newLabel); 207 m_panel.add(m_newBox); 208 Label changeLabel = createLabel(aliasMessages.existingAliases()); 209 m_panel.add(changeLabel); 210 m_panel.add(m_changeBox); 211 init(aliases); 212 } 213 214 /** 215 * Adds the controls for a single alias to the widget.<p> 216 * 217 * @param alias the alias for which the controls should be added 218 */ 219 public void addAlias(final CmsAliasBean alias) { 220 221 final HorizontalPanel hp = new HorizontalPanel(); 222 hp.getElement().getStyle().setMargin(2, Unit.PX); 223 final CmsTextBox textbox = createTextBox(); 224 textbox.setFormValueAsString(alias.getSitePath()); 225 hp.add(textbox); 226 227 CmsSelectBox selectbox = createSelectBox(); 228 selectbox.setFormValueAsString(alias.getMode().toString()); 229 hp.add(selectbox); 230 PushButton deleteButton = createDeleteButton(); 231 hp.add(deleteButton); 232 233 final AliasControls controls = new AliasControls(alias, textbox, selectbox); 234 m_aliasControls.put(controls.getId(), controls); 235 236 selectbox.addValueChangeHandler(new ValueChangeHandler<String>() { 237 238 public void onValueChange(ValueChangeEvent<String> event) { 239 240 onChangePath(controls); 241 } 242 }); 243 244 deleteButton.addClickHandler(new ClickHandler() { 245 246 public void onClick(ClickEvent e) { 247 248 m_aliasControls.remove(controls.getId()); 249 hp.removeFromParent(); 250 validateFull(m_structureId, getAliasPaths(), m_defaultValidationHandler); 251 } 252 }); 253 textbox.addValueChangeHandler(new ValueChangeHandler<String>() { 254 255 public void onValueChange(ValueChangeEvent<String> e) { 256 257 onChangePath(controls); 258 validateFull(m_structureId, getAliasPaths(), m_defaultValidationHandler); 259 } 260 }); 261 262 textbox.addKeyPressHandler(new KeyPressHandler() { 263 264 public void onKeyPress(KeyPressEvent event) { 265 266 onChangePath(controls); 267 } 268 }); 269 m_changeBox.add(hp); 270 CmsDomUtil.resizeAncestor(this); 271 } 272 273 /** 274 * Clears the validation error flag.<p> 275 */ 276 public void clearValidationErrors() { 277 278 m_hasValidationErrors = false; 279 } 280 281 /** 282 * Gets a list of the changed aliases.<p> 283 * 284 * @return a list of the aliases 285 */ 286 public List<CmsAliasBean> getAliases() { 287 288 List<CmsAliasBean> beans = new ArrayList<CmsAliasBean>(); 289 for (AliasControls controls : m_aliasControls.values()) { 290 beans.add(controls.getAlias()); 291 } 292 return beans; 293 } 294 295 /** 296 * Gets a map of the current alias site paths, with the alias controls ids as the keys.<p> 297 * 298 * @return a map from control ids to alias site paths 299 */ 300 public Map<String, String> getAliasPaths() { 301 302 Map<String, String> paths = new HashMap<String, String>(); 303 for (AliasControls controls : m_aliasControls.values()) { 304 paths.put(controls.getId(), controls.getAlias().getSitePath()); 305 } 306 return paths; 307 } 308 309 /** 310 * Checks whether there have been validation errors since the validation errors were cleared the last time.<p> 311 * 312 * @return true if there were validation errors 313 */ 314 public boolean hasValidationErrors() { 315 316 return m_hasValidationErrors; 317 } 318 319 /** 320 * Initializes the alias controls.<p> 321 * 322 * @param aliases the existing aliases 323 */ 324 public void init(List<CmsAliasBean> aliases) { 325 326 for (CmsAliasBean alias : aliases) { 327 addAlias(alias); 328 } 329 330 final HorizontalPanel hp = new HorizontalPanel(); 331 final CmsTextBox textbox = createTextBox(); 332 textbox.setGhostMode(true); 333 textbox.setGhostValue(aliasMessages.enterAlias(), true); 334 textbox.setGhostModeClear(true); 335 hp.add(textbox); 336 final CmsSelectBox selectbox = createSelectBox(); 337 hp.add(selectbox); 338 PushButton addButton = createAddButton(); 339 hp.add(addButton); 340 final Runnable addAction = new Runnable() { 341 342 public void run() { 343 344 textbox.setErrorMessage(null); 345 validateSingle(m_structureId, getAliasPaths(), textbox.getText(), new AsyncCallback<String>() { 346 347 public void onFailure(Throwable caught) { 348 349 // shouldn't be called 350 } 351 352 public void onSuccess(String result) { 353 354 if (result == null) { 355 CmsAliasMode mode = CmsAliasMode.valueOf(selectbox.getFormValueAsString()); 356 addAlias(new CmsAliasBean(textbox.getText(), mode)); 357 textbox.setFormValueAsString(""); 358 } else { 359 textbox.setErrorMessage(result); 360 } 361 } 362 }); 363 } 364 }; 365 366 ClickHandler clickHandler = new ClickHandler() { 367 368 public void onClick(ClickEvent e) { 369 370 addAction.run(); 371 } 372 }; 373 addButton.addClickHandler(clickHandler); 374 textbox.addKeyPressHandler(new KeyPressHandler() { 375 376 public void onKeyPress(KeyPressEvent event) { 377 378 int keycode = event.getNativeEvent().getKeyCode(); 379 if ((keycode == 10) || (keycode == 13)) { 380 addAction.run(); 381 } 382 } 383 }); 384 385 m_newBox.add(hp); 386 } 387 388 /** 389 * Simplified method to perform a full validation of the aliases in the list and execute an action afterwards.<p> 390 * 391 * @param nextAction the action to execute after the validation finished 392 */ 393 public void validate(final Runnable nextAction) { 394 395 validateFull(m_structureId, getAliasPaths(), new AsyncCallback<Map<String, String>>() { 396 397 public void onFailure(Throwable caught) { 398 399 // do nothing 400 401 } 402 403 public void onSuccess(Map<String, String> result) { 404 405 for (Map.Entry<String, String> entry : result.entrySet()) { 406 if (entry.getValue() != null) { 407 m_hasValidationErrors = true; 408 } 409 } 410 m_defaultValidationHandler.onSuccess(result); 411 nextAction.run(); 412 } 413 }); 414 } 415 416 /** 417 * Validates aliases. 418 * 419 * @param uuid The structure id for which the aliases should be valid 420 * @param aliasPaths a map from id strings to alias paths 421 * @param callback the callback which should be called with the validation results 422 */ 423 public void validateAliases( 424 final CmsUUID uuid, 425 final Map<String, String> aliasPaths, 426 final AsyncCallback<Map<String, String>> callback) { 427 428 CmsRpcAction<Map<String, String>> action = new CmsRpcAction<Map<String, String>>() { 429 430 /** 431 * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute() 432 */ 433 @Override 434 public void execute() { 435 436 start(200, true); 437 CmsCoreProvider.getVfsService().validateAliases(uuid, aliasPaths, this); 438 } 439 440 /** 441 * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object) 442 */ 443 @Override 444 protected void onResponse(Map<String, String> result) { 445 446 stop(false); 447 callback.onSuccess(result); 448 } 449 450 }; 451 action.execute(); 452 } 453 454 /** 455 * Creates the button used for adding new aliases.<p> 456 * 457 * @return the new button 458 */ 459 protected PushButton createAddButton() { 460 461 PushButton button = createIconButton(I_CmsButton.ADD_SMALL); 462 button.setTitle(aliasMessages.addAlias()); 463 return button; 464 } 465 466 /** 467 * Creates the button used for deleting aliases.<p> 468 * 469 * @return the new button 470 */ 471 protected PushButton createDeleteButton() { 472 473 PushButton button = createIconButton(I_CmsButton.DELETE_SMALL); 474 button.setTitle(aliasMessages.removeAlias()); 475 return button; 476 } 477 478 /** 479 * Creates an icon button for editing aliases.<p> 480 * 481 * @param icon the icon css class to use 482 * 483 * @return the new icon button 484 */ 485 protected PushButton createIconButton(String icon) { 486 487 CmsPushButton button = new CmsPushButton(); 488 button.setImageClass(icon); 489 button.setButtonStyle(ButtonStyle.FONT_ICON, null); 490 return button; 491 } 492 493 /** 494 * Creates a label for this widget.<p> 495 * 496 * @param text the text to display in the label 497 * 498 * @return the created label 499 */ 500 protected Label createLabel(String text) { 501 502 Label label = new Label(text); 503 Style style = label.getElement().getStyle(); 504 style.setMarginTop(10, Unit.PX); 505 style.setMarginBottom(4, Unit.PX); 506 style.setFontWeight(FontWeight.BOLD); 507 return label; 508 } 509 510 /** 511 * Creates the select box for selecting alias modes.<p> 512 * 513 * @return the select box for selecting alias modes 514 */ 515 protected CmsSelectBox createSelectBox() { 516 517 CmsSelectBox selectbox = new CmsSelectBox(); 518 selectbox.setTitle(CmsAliasMode.page.toString(), aliasMessages.pageDescription()); 519 selectbox.setTitle(CmsAliasMode.redirect.toString(), aliasMessages.redirectDescription()); 520 selectbox.setTitle(CmsAliasMode.permanentRedirect.toString(), aliasMessages.movedDescription()); 521 selectbox.addOption(CmsAliasMode.page.toString(), aliasMessages.optionPage()); 522 selectbox.addOption(CmsAliasMode.redirect.toString(), aliasMessages.optionRedirect()); 523 selectbox.addOption(CmsAliasMode.permanentRedirect.toString(), aliasMessages.optionMoved()); 524 525 selectbox.getElement().getStyle().setWidth(190, Unit.PX); 526 selectbox.getElement().getStyle().setMarginRight(5, Unit.PX); 527 return selectbox; 528 } 529 530 /** 531 * Creates a text box for entering an alias path.<p> 532 * 533 * @return the new text box 534 */ 535 protected CmsTextBox createTextBox() { 536 537 CmsTextBox textbox = new CmsTextBox(); 538 textbox.getElement().getStyle().setWidth(325, Unit.PX); 539 textbox.getElement().getStyle().setMarginRight(5, Unit.PX); 540 return textbox; 541 } 542 543 /** 544 * This method is called when an alias path changes.<p> 545 * 546 * @param controls the alias controls 547 */ 548 protected void onChangePath(AliasControls controls) { 549 550 CmsTextBox textbox = controls.getTextBox(); 551 CmsAliasBean alias = controls.getAlias(); 552 CmsSelectBox selectbox = controls.getSelectBox(); 553 String text = textbox.getText(); 554 alias.setSitePath(text); 555 alias.setMode(CmsAliasMode.valueOf(selectbox.getFormValueAsString())); 556 } 557 558 /** 559 * Performs a validation of the current list of aliases in the widget.<p> 560 * 561 * @param structureId the resource's structure id 562 * @param sitePaths the map from ids to alias site paths 563 * 564 * @param errorCallback the callback to invoke when the validation finishes 565 */ 566 protected void validateFull( 567 CmsUUID structureId, 568 Map<String, String> sitePaths, 569 final AsyncCallback<Map<String, String>> errorCallback) { 570 571 validateAliases(structureId, sitePaths, errorCallback); 572 } 573 574 /** 575 * Validation method used when adding a new alias.<p> 576 * 577 * @param structureId the structure id 578 * @param sitePaths the site paths 579 * @param newSitePath the new site path 580 * @param errorCallback on error callback 581 */ 582 protected void validateSingle( 583 CmsUUID structureId, 584 Map<String, String> sitePaths, 585 String newSitePath, 586 final AsyncCallback<String> errorCallback) { 587 588 Map<String, String> newMap = new HashMap<String, String>(sitePaths); 589 newMap.put("NEW", newSitePath); //$NON-NLS-1$ 590 AsyncCallback<Map<String, String>> callback = new AsyncCallback<Map<String, String>>() { 591 592 public void onFailure(Throwable caught) { 593 594 assert false; // should never happen 595 } 596 597 public void onSuccess(Map<String, String> result) { 598 599 String newRes = result.get("NEW"); //$NON-NLS-1$ 600 errorCallback.onSuccess(newRes); 601 } 602 }; 603 validateAliases(structureId, newMap, callback); 604 } 605}