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}