001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://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.ade.sitemap.client.control;
029
030import org.opencms.ade.detailpage.CmsDetailPageInfo;
031import org.opencms.ade.sitemap.client.CmsSitemapTreeItem;
032import org.opencms.ade.sitemap.client.CmsSitemapView;
033import org.opencms.ade.sitemap.client.Messages;
034import org.opencms.ade.sitemap.client.edit.CmsLocaleComparePropertyHandler;
035import org.opencms.ade.sitemap.client.edit.CmsNavModePropertyEditor;
036import org.opencms.ade.sitemap.shared.CmsClientSitemapEntry;
037import org.opencms.ade.sitemap.shared.CmsDetailPageTable;
038import org.opencms.ade.sitemap.shared.CmsGalleryFolderEntry;
039import org.opencms.ade.sitemap.shared.CmsGalleryType;
040import org.opencms.ade.sitemap.shared.CmsLocaleComparePropertyData;
041import org.opencms.ade.sitemap.shared.CmsModelInfo;
042import org.opencms.ade.sitemap.shared.CmsModelPageEntry;
043import org.opencms.ade.sitemap.shared.CmsNewResourceInfo;
044import org.opencms.ade.sitemap.shared.CmsSitemapCategoryData;
045import org.opencms.ade.sitemap.shared.CmsSitemapChange;
046import org.opencms.ade.sitemap.shared.CmsSitemapChange.ChangeType;
047import org.opencms.ade.sitemap.shared.CmsSitemapClipboardData;
048import org.opencms.ade.sitemap.shared.CmsSitemapData;
049import org.opencms.ade.sitemap.shared.CmsSitemapData.EditorMode;
050import org.opencms.ade.sitemap.shared.I_CmsSitemapController;
051import org.opencms.ade.sitemap.shared.rpc.I_CmsSitemapService;
052import org.opencms.ade.sitemap.shared.rpc.I_CmsSitemapServiceAsync;
053import org.opencms.file.CmsResource;
054import org.opencms.gwt.client.CmsCoreProvider;
055import org.opencms.gwt.client.property.A_CmsPropertyEditor;
056import org.opencms.gwt.client.property.CmsPropertySubmitHandler;
057import org.opencms.gwt.client.property.CmsReloadMode;
058import org.opencms.gwt.client.property.definition.CmsPropertyDefinitionButton;
059import org.opencms.gwt.client.rpc.CmsRpcAction;
060import org.opencms.gwt.client.rpc.CmsRpcPrefetcher;
061import org.opencms.gwt.client.ui.CmsDeleteWarningDialog;
062import org.opencms.gwt.client.ui.CmsErrorDialog;
063import org.opencms.gwt.client.ui.input.form.CmsDialogFormHandler;
064import org.opencms.gwt.client.ui.input.form.CmsFormDialog;
065import org.opencms.gwt.client.ui.input.form.I_CmsFormSubmitHandler;
066import org.opencms.gwt.client.ui.tree.CmsLazyTreeItem.LoadState;
067import org.opencms.gwt.client.util.CmsDebugLog;
068import org.opencms.gwt.client.util.I_CmsSimpleCallback;
069import org.opencms.gwt.shared.CmsCoreData;
070import org.opencms.gwt.shared.CmsListInfoBean;
071import org.opencms.gwt.shared.property.CmsClientProperty;
072import org.opencms.gwt.shared.property.CmsPropertyModification;
073import org.opencms.gwt.shared.rpc.I_CmsVfsServiceAsync;
074import org.opencms.util.CmsStringUtil;
075import org.opencms.util.CmsUUID;
076import org.opencms.xml.content.CmsXmlContentProperty;
077
078import java.util.ArrayList;
079import java.util.Collection;
080import java.util.Collections;
081import java.util.HashMap;
082import java.util.HashSet;
083import java.util.LinkedHashMap;
084import java.util.LinkedList;
085import java.util.List;
086import java.util.Map;
087import java.util.Set;
088
089import com.google.common.collect.Maps;
090import com.google.gwt.core.client.GWT;
091import com.google.gwt.dom.client.Style;
092import com.google.gwt.event.shared.HandlerRegistration;
093import com.google.gwt.event.shared.SimpleEventBus;
094import com.google.gwt.user.client.Command;
095import com.google.gwt.user.client.DOM;
096import com.google.gwt.user.client.Window;
097import com.google.gwt.user.client.rpc.AsyncCallback;
098import com.google.gwt.user.client.rpc.SerializationException;
099import com.google.gwt.user.client.rpc.ServiceDefTarget;
100
101/**
102 * Sitemap editor controller.<p>
103 *
104 * @since 8.0.0
105 */
106public class CmsSitemapController implements I_CmsSitemapController {
107
108    /** The name to use for new entries. */
109    public static final String NEW_ENTRY_NAME = "page";
110
111    /** A map of *all* detail page info beans, indexed by page id. */
112    protected Map<CmsUUID, CmsDetailPageInfo> m_allDetailPageInfos = new HashMap<CmsUUID, CmsDetailPageInfo>();
113
114    /** The sitemap data. */
115    protected CmsSitemapData m_data;
116
117    /** The detail page table. */
118    protected CmsDetailPageTable m_detailPageTable;
119
120    /** The event bus. */
121    protected SimpleEventBus m_eventBus;
122
123    /** The category data. */
124    CmsSitemapCategoryData m_categoryData;
125
126    /** The entry data model. */
127    private Map<CmsUUID, CmsClientSitemapEntry> m_entriesById;
128
129    /** The sitemap entries by path. */
130    private Map<String, CmsClientSitemapEntry> m_entriesByPath;
131
132    /** The gallery type names by id. */
133    private Map<Integer, CmsGalleryType> m_galleryTypes;
134
135    /** The set of names of hidden properties. */
136    private Set<String> m_hiddenProperties;
137
138    /** The map of property maps by structure id. */
139    private Map<CmsUUID, Map<String, CmsClientProperty>> m_propertyMaps = Maps.newHashMap();
140
141    /** The list of property update handlers. */
142    private List<I_CmsPropertyUpdateHandler> m_propertyUpdateHandlers = new ArrayList<I_CmsPropertyUpdateHandler>();
143
144    /** The sitemap service instance. */
145    private I_CmsSitemapServiceAsync m_service;
146
147    /** The vfs service. */
148    private I_CmsVfsServiceAsync m_vfsService;
149
150    /**
151     * Constructor.<p>
152     */
153    public CmsSitemapController() {
154
155        m_entriesById = new HashMap<CmsUUID, CmsClientSitemapEntry>();
156        m_entriesByPath = new HashMap<String, CmsClientSitemapEntry>();
157        m_galleryTypes = new HashMap<Integer, CmsGalleryType>();
158        try {
159            m_data = (CmsSitemapData)CmsRpcPrefetcher.getSerializedObjectFromDictionary(
160                getService(),
161                CmsSitemapData.DICT_NAME);
162        } catch (SerializationException e) {
163            CmsErrorDialog.handleException(
164                new Exception(
165                    "Deserialization of sitemap data failed. This may be caused by expired java-script resources, please clear your browser cache and try again.",
166                    e));
167        }
168
169        m_hiddenProperties = new HashSet<String>();
170        if (m_data != null) {
171            m_detailPageTable = m_data.getDetailPageTable();
172            m_data.getRoot().initializeAll(this);
173            m_eventBus = new SimpleEventBus();
174            initDetailPageInfos();
175        }
176    }
177
178    /**
179     * Opens the property editor for locale compare mode.<p>
180     *
181     * @param data the data used by the property editor
182     * @param ownId the id
183     * @param defaultFileId the default file id
184     * @param noEditReason the reason the properties can't be edited
185     */
186    public static void editPropertiesForLocaleCompareMode(
187        final CmsLocaleComparePropertyData data,
188        final CmsUUID ownId,
189        final CmsUUID defaultFileId,
190        final String noEditReason) {
191
192        final CmsUUID infoId;
193        infoId = ownId;
194        Set<CmsUUID> idsForPropertyConfig = new HashSet<CmsUUID>();
195        idsForPropertyConfig.add(defaultFileId);
196        idsForPropertyConfig.add(ownId);
197
198        final List<CmsUUID> propertyConfigIds = new ArrayList<CmsUUID>(idsForPropertyConfig);
199
200        CmsRpcAction<CmsListInfoBean> action = new CmsRpcAction<CmsListInfoBean>() {
201
202            @Override
203            public void execute() {
204
205                start(0, true);
206                CmsCoreProvider.getVfsService().getPageInfo(infoId, this);
207            }
208
209            @Override
210            protected void onResponse(CmsListInfoBean infoResult) {
211
212                stop(false);
213                CmsRpcAction<CmsListInfoBean> defaultFileInfoAction = new CmsRpcAction<CmsListInfoBean>() {
214
215                    @Override
216                    public void execute() {
217
218                        start(0, true);
219                        CmsCoreProvider.getVfsService().getPageInfo(defaultFileId, this);
220                    }
221
222                    @Override
223                    protected void onResponse(CmsListInfoBean defaultFileInfo) {
224
225                        stop(false);
226
227                        final CmsLocaleComparePropertyHandler handler = new CmsLocaleComparePropertyHandler(data);
228                        handler.setPageInfo(infoResult);
229                        handler.setDefaultFilePath(defaultFileInfo.getSubTitle());
230                        CmsRpcAction<Map<CmsUUID, Map<String, CmsXmlContentProperty>>> propertyAction = new CmsRpcAction<Map<CmsUUID, Map<String, CmsXmlContentProperty>>>() {
231
232                            @Override
233                            public void execute() {
234
235                                start(0, true);
236                                CmsCoreProvider.getVfsService().getDefaultProperties(propertyConfigIds, this);
237                            }
238
239                            @Override
240                            protected void onResponse(Map<CmsUUID, Map<String, CmsXmlContentProperty>> propertyResult) {
241
242                                stop(false);
243                                Map<String, CmsXmlContentProperty> propConfig = new LinkedHashMap<String, CmsXmlContentProperty>();
244                                for (Map<String, CmsXmlContentProperty> defaultProps : propertyResult.values()) {
245                                    propConfig.putAll(defaultProps);
246                                }
247                                propConfig.putAll(
248                                    CmsSitemapView.getInstance().getController().getData().getProperties());
249                                A_CmsPropertyEditor editor = new CmsNavModePropertyEditor(propConfig, handler);
250                                editor.setPropertyNames(
251                                    CmsSitemapView.getInstance().getController().getData().getAllPropertyNames());
252                                final CmsFormDialog dialog = new CmsFormDialog(
253                                    handler.getDialogTitle(),
254                                    editor.getForm());
255                                CmsPropertyDefinitionButton defButton = new CmsPropertyDefinitionButton() {
256
257                                    /**
258                                     * @see org.opencms.gwt.client.property.definition.CmsPropertyDefinitionButton#onBeforeEditPropertyDefinition()
259                                     */
260                                    @Override
261                                    public void onBeforeEditPropertyDefinition() {
262
263                                        dialog.hide();
264                                    }
265
266                                };
267                                defButton.getElement().getStyle().setFloat(Style.Float.LEFT);
268                                defButton.installOnDialog(dialog);
269                                CmsDialogFormHandler formHandler = new CmsDialogFormHandler();
270                                formHandler.setDialog(dialog);
271                                I_CmsFormSubmitHandler submitHandler = new CmsPropertySubmitHandler(handler);
272                                formHandler.setSubmitHandler(submitHandler);
273                                dialog.setFormHandler(formHandler);
274                                editor.initializeWidgets(dialog);
275                                dialog.centerHorizontally(50);
276                                dialog.catchNotifications();
277                                if (noEditReason != null) {
278                                    editor.disableInput(noEditReason, false);
279                                    dialog.getOkButton().disable(noEditReason);
280                                }
281
282                            }
283                        };
284                        propertyAction.execute();
285                    }
286                };
287                defaultFileInfoAction.execute();
288
289            }
290
291        };
292        action.execute();
293    }
294
295    /**
296     * Helper method for looking up a value in a map which may be null.<p>
297     *
298     * @param <A> the key type
299     * @param <B> the value type
300     * @param map the map (which may be null)
301     * @param key the map key
302     *
303     * @return the value of the map at the given key, or null if the map is null
304     */
305    public static <A, B> B safeLookup(Map<A, B> map, A key) {
306
307        if (map == null) {
308            return null;
309        }
310        return map.get(key);
311    }
312
313    /**
314     * Adds a new change event handler.<p>
315     *
316     * @param handler the handler to add
317     *
318     * @return the handler registration
319     */
320    public HandlerRegistration addChangeHandler(I_CmsSitemapChangeHandler handler) {
321
322        return m_eventBus.addHandlerToSource(CmsSitemapChangeEvent.getType(), this, handler);
323    }
324
325    /**
326     * Adds a new detail page information bean.<p>
327     *
328     * @param info the detail page information bean to add
329     */
330    public void addDetailPageInfo(CmsDetailPageInfo info) {
331
332        m_detailPageTable.add(info);
333        m_allDetailPageInfos.put(info.getId(), info);
334    }
335
336    /**
337     * Adds a new load event handler.<p>
338     *
339     * @param handler the handler to add
340     *
341     * @return the handler registration
342     */
343    public HandlerRegistration addLoadHandler(I_CmsSitemapLoadHandler handler) {
344
345        return m_eventBus.addHandlerToSource(CmsSitemapLoadEvent.getType(), this, handler);
346    }
347
348    /**
349     * Adds a handler for property changes caused by user edits.<p>
350     *
351     * @param handler a new handler for property updates caused by the user
352     */
353    public void addPropertyUpdateHandler(I_CmsPropertyUpdateHandler handler) {
354
355        m_propertyUpdateHandlers.add(handler);
356
357    }
358
359    /**
360     * Adds the entry to the navigation.<p>
361     *
362     * @param entry the entry
363     */
364    public void addToNavigation(CmsClientSitemapEntry entry) {
365
366        entry.setInNavigation(true);
367        CmsSitemapChange change = new CmsSitemapChange(entry.getId(), entry.getSitePath(), ChangeType.modify);
368        CmsPropertyModification mod = new CmsPropertyModification(
369            entry.getId(),
370            CmsClientProperty.PROPERTY_NAVTEXT,
371            entry.getTitle(),
372            true);
373        change.setPropertyChanges(Collections.singletonList(mod));
374        change.setPosition(entry.getPosition());
375        commitChange(change, null);
376    }
377
378    /**
379     * Makes the given sitemap entry the default detail page for its detail page type.<p>
380     *
381     * @param entry an entry representing a detail page
382     */
383    public void bump(CmsClientSitemapEntry entry) {
384
385        CmsDetailPageTable table = getDetailPageTable().copy();
386        table.makeDefault(entry.getId());
387        CmsSitemapChange change = new CmsSitemapChange(entry.getId(), entry.getSitePath(), ChangeType.bumpDetailPage);
388        change.setDetailPageInfos(table.toList());
389        commitChange(change, null);
390    }
391
392    /**
393     * Checks if the user has a sufficient role for managing detail pages.
394     *
395     * @return true if the user has a sufficient role for managing detail pages
396     */
397    public boolean canManageDetailPages() {
398
399        return m_data.canManagetDetailPages();
400    }
401
402    /**
403     * Changes the given category.<p>
404     *
405     * @param id the category id
406     * @param title the new category title
407     * @param name the new category name
408     */
409    public void changeCategory(final CmsUUID id, final String title, final String name) {
410
411        CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
412
413            @Override
414            public void execute() {
415
416                start(200, true);
417                getService().changeCategory(getEntryPoint(), id, title, name, this);
418            }
419
420            @Override
421            protected void onResponse(Void result) {
422
423                stop(false);
424                loadCategories(true, null);
425            }
426
427        };
428        action.execute();
429    }
430
431    /**
432     * Clears the deleted clip-board list and commits the change.<p>
433     */
434    public void clearDeletedList() {
435
436        CmsSitemapClipboardData clipboardData = getData().getClipboardData().copy();
437        clipboardData.getDeletions().clear();
438        CmsSitemapChange change = new CmsSitemapChange(null, null, ChangeType.clipboardOnly);
439        change.setClipBoardData(clipboardData);
440        commitChange(change, null);
441    }
442
443    /**
444     * Clears the modified clip-board list and commits the change.<p>
445     */
446    public void clearModifiedList() {
447
448        CmsSitemapClipboardData clipboardData = getData().getClipboardData().copy();
449        clipboardData.getModifications().clear();
450        CmsSitemapChange change = new CmsSitemapChange(null, null, ChangeType.clipboardOnly);
451        change.setClipBoardData(clipboardData);
452        commitChange(change, null);
453    }
454
455    /**
456     * Registers a new sitemap entry.<p>
457     *
458     * @param newEntry the new entry
459     * @param parentId the parent entry id
460     * @param resourceTypeId the resource type id
461     * @param copyResourceId the copy resource id
462     * @param parameter an additional parameter which may contain more information needed to create the new resource
463     * @param isNewSitemap a flag controlling whether a new sitemap should be created
464     */
465    public void create(
466        CmsClientSitemapEntry newEntry,
467        CmsUUID parentId,
468        int resourceTypeId,
469        CmsUUID copyResourceId,
470        String parameter,
471        boolean isNewSitemap) {
472
473        assert (getEntry(newEntry.getSitePath()) == null);
474
475        CmsSitemapChange change = new CmsSitemapChange(null, newEntry.getSitePath(), ChangeType.create);
476        change.setDefaultFileId(newEntry.getDefaultFileId());
477        change.setParentId(parentId);
478        change.setName(newEntry.getName());
479        change.setPosition(newEntry.getPosition());
480        change.setOwnInternalProperties(newEntry.getOwnProperties());
481        change.setDefaultFileInternalProperties(newEntry.getDefaultFileProperties());
482        change.setTitle(newEntry.getTitle());
483        change.setCreateParameter(parameter);
484        change.setNewResourceTypeId(resourceTypeId);
485        if (isNewSitemap) {
486            change.setCreateSitemapFolderType(newEntry.getResourceTypeName());
487        }
488        change.setNewCopyResourceId(copyResourceId);
489        if (isDetailPage(newEntry)) {
490            CmsDetailPageTable table = getDetailPageTable().copy();
491            if (!table.contains(newEntry.getId())) {
492                CmsDetailPageInfo info = new CmsDetailPageInfo(
493                    newEntry.getId(),
494                    newEntry.getSitePath(),
495                    newEntry.getDetailpageTypeName(),
496                    /* qualifier = */null,
497                    new ArrayList<>(),
498                    newEntry.getVfsModeIcon());
499                table.add(info);
500            }
501            change.setDetailPageInfos(table.toList());
502        }
503        CmsSitemapClipboardData data = getData().getClipboardData().copy();
504        data.addModified(newEntry);
505        change.setClipBoardData(data);
506        commitChange(change, null);
507    }
508
509    /**
510     * Creates a new category.<p>
511     *
512     * @param id the parent category id, or the null uuid for a new top-level category
513     * @param title the title of the category
514     * @param name the name of the category
515     */
516    public void createCategory(final CmsUUID id, final String title, final String name) {
517
518        CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
519
520            @Override
521            public void execute() {
522
523                start(200, true);
524                getService().createCategory(getEntryPoint(), id, title, name, this);
525            }
526
527            @Override
528            protected void onResponse(Void result) {
529
530                stop(false);
531                loadCategories(true, id);
532
533            }
534        };
535        action.execute();
536    }
537
538    /**
539     * Creates a new gallery folder of the given type.<p>
540     *
541     * @param parentId the parent folder id
542     * @param galleryTypeId the folder type id
543     * @param title the folder title
544     */
545    public void createNewGallery(final CmsUUID parentId, final int galleryTypeId, final String title) {
546
547        final String parentFolder = parentId != null
548        ? getEntryById(parentId).getSitePath()
549        : CmsStringUtil.joinPaths(m_data.getRoot().getSitePath(), m_data.getDefaultGalleryFolder());
550
551        CmsRpcAction<CmsGalleryFolderEntry> action = new CmsRpcAction<CmsGalleryFolderEntry>() {
552
553            @Override
554            public void execute() {
555
556                getService().createNewGalleryFolder(parentFolder, title, galleryTypeId, this);
557            }
558
559            @Override
560            protected void onResponse(CmsGalleryFolderEntry result) {
561
562                CmsSitemapView.getInstance().displayNewGallery(result);
563
564            }
565        };
566        action.execute();
567    }
568
569    /**
570     * Creates a new model page.<p>
571     *
572     * @param title the title of the model page
573     * @param description the description of the model page
574     * @param copyId the structure id of the resource which should be used as a copy model for the new page
575     * @param isModelGroup in case of a model group page
576     */
577    public void createNewModelPage(
578        final String title,
579        final String description,
580        final CmsUUID copyId,
581        final boolean isModelGroup) {
582
583        CmsRpcAction<CmsModelPageEntry> action = new CmsRpcAction<CmsModelPageEntry>() {
584
585            @Override
586            public void execute() {
587
588                start(200, true);
589
590                getService().createNewModelPage(getEntryPoint(), title, description, copyId, isModelGroup, this);
591
592            }
593
594            @Override
595            protected void onResponse(final CmsModelPageEntry result) {
596
597                stop(false);
598                if (isModelGroup) {
599                    CmsSitemapView.getInstance().displayNewModelPage(result, true);
600                } else {
601                    loadNewElementInfo(new AsyncCallback<Void>() {
602
603                        public void onFailure(Throwable caught) {
604
605                            // nothing to do
606
607                        }
608
609                        public void onSuccess(Void v) {
610
611                            CmsSitemapView.getInstance().displayNewModelPage(result, false);
612                        }
613                    });
614                }
615
616            }
617
618        };
619        action.execute();
620    }
621
622    /**
623     * Creates a sitemap folder.<p>
624     *
625     * @param newEntry the new entry
626     * @param parentId the entry parent id
627     * @param sitemapType the resource type for the subsitemap folder
628     */
629    public void createSitemapSubEntry(final CmsClientSitemapEntry newEntry, CmsUUID parentId, String sitemapType) {
630
631        CmsUUID structureId = m_data.getDefaultNewElementInfo().getCopyResourceId();
632        newEntry.setResourceTypeName(sitemapType);
633        create(newEntry, parentId, m_data.getDefaultNewElementInfo().getId(), structureId, null, true);
634    }
635
636    /**
637     * Creates a new sub-entry which is a subsitemap.<p>
638     *
639     * @param parent the parent entry
640     * @param sitemapFolderType the sitemap folder type
641     */
642    public void createSitemapSubEntry(final CmsClientSitemapEntry parent, final String sitemapFolderType) {
643
644        CmsSitemapTreeItem item = CmsSitemapTreeItem.getItemById(parent.getId());
645        AsyncCallback<CmsClientSitemapEntry> callback = new AsyncCallback<CmsClientSitemapEntry>() {
646
647            public void onFailure(Throwable caught) {
648
649                // do nothing
650            }
651
652            public void onSuccess(CmsClientSitemapEntry result) {
653
654                makeNewEntry(parent, new I_CmsSimpleCallback<CmsClientSitemapEntry>() {
655
656                    public void execute(CmsClientSitemapEntry newEntry) {
657
658                        newEntry.setResourceTypeName(sitemapFolderType);
659                        createSitemapSubEntry(newEntry, parent.getId(), sitemapFolderType);
660                    }
661                });
662            }
663        };
664        if (item.getLoadState().equals(LoadState.UNLOADED)) {
665            getChildren(parent.getId(), true, callback);
666        } else {
667            callback.onSuccess(parent);
668        }
669    }
670
671    /**
672     * Creates a new sub-entry of an existing sitemap entry.<p>
673     *
674     * @param parent the entry to which a new sub-entry should be added
675     */
676    public void createSubEntry(final CmsClientSitemapEntry parent) {
677
678        createSubEntry(parent, null);
679    }
680
681    /**
682     * Creates a new sub-entry of an existing sitemap entry.<p>
683     *
684     * @param parent the entry to which a new sub-entry should be added
685     * @param structureId the structure id of the model page (if null, uses default model page)
686     */
687    public void createSubEntry(final CmsClientSitemapEntry parent, final CmsUUID structureId) {
688
689        CmsSitemapTreeItem item = CmsSitemapTreeItem.getItemById(parent.getId());
690        AsyncCallback<CmsClientSitemapEntry> callback = new AsyncCallback<CmsClientSitemapEntry>() {
691
692            public void onFailure(Throwable caught) {
693
694                // nothing to do
695            }
696
697            public void onSuccess(CmsClientSitemapEntry result) {
698
699                makeNewEntry(parent, new I_CmsSimpleCallback<CmsClientSitemapEntry>() {
700
701                    public void execute(CmsClientSitemapEntry newEntry) {
702
703                        createSubEntry(newEntry, parent.getId(), structureId);
704                    }
705                });
706
707            }
708
709        };
710
711        if (item.getLoadState().equals(LoadState.UNLOADED)) {
712            getChildren(parent.getId(), true, callback);
713        } else {
714            callback.onSuccess(parent);
715        }
716    }
717
718    /**
719     * Registers a new sitemap entry.<p>
720     *
721     * @param newEntry the new entry
722     * @param parentId the parent entry id
723     * @param structureId the structure id of the model page (if null, uses default model page)
724     */
725    public void createSubEntry(final CmsClientSitemapEntry newEntry, CmsUUID parentId, CmsUUID structureId) {
726
727        if (structureId == null) {
728            structureId = m_data.getDefaultNewElementInfo().getCopyResourceId();
729        }
730        create(newEntry, parentId, m_data.getDefaultNewElementInfo().getId(), structureId, null, false);
731    }
732
733    /**
734     * Creates a sub-sitemap from the subtree of the current sitemap starting at the given entry.<p>
735     *
736     * @param entryId the id of the entry
737     */
738    public void createSubSitemap(final CmsUUID entryId) {
739
740        CmsRpcAction<CmsSitemapChange> subSitemapAction = new CmsRpcAction<CmsSitemapChange>() {
741
742            /**
743             * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
744             */
745            @Override
746            public void execute() {
747
748                start(0, true);
749                getService().createSubSitemap(entryId, this);
750            }
751
752            /**
753             * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
754             */
755            @Override
756            protected void onResponse(CmsSitemapChange result) {
757
758                stop(false);
759                applyChange(result);
760            }
761        };
762        subSitemapAction.execute();
763    }
764
765    /**
766     * Deletes the given entry and all its descendants.<p>
767     *
768     * @param sitePath the site path of the entry to delete
769     */
770    public void delete(String sitePath) {
771
772        CmsClientSitemapEntry entry = getEntry(sitePath);
773        CmsClientSitemapEntry parent = getEntry(CmsResource.getParentFolder(entry.getSitePath()));
774        CmsSitemapChange change = new CmsSitemapChange(entry.getId(), entry.getSitePath(), ChangeType.delete);
775        change.setParentId(parent.getId());
776        change.setDefaultFileId(entry.getDefaultFileId());
777        CmsSitemapClipboardData data = CmsSitemapView.getInstance().getController().getData().getClipboardData().copy();
778        if (!entry.isNew()) {
779            data.addDeleted(entry);
780            removeDeletedFromModified(entry, data);
781        }
782        change.setClipBoardData(data);
783        CmsDetailPageTable detailPageTable = CmsSitemapView.getInstance().getController().getData().getDetailPageTable();
784        CmsUUID id = entry.getId();
785        if (detailPageTable.contains(id)) {
786            CmsDetailPageTable copyTable = detailPageTable.copy();
787            copyTable.remove(id);
788            change.setDetailPageInfos(copyTable.toList());
789        }
790        commitChange(change, null);
791    }
792
793    /**
794     * Deletes a category.<p>
795     *
796     * @param id the id of the category
797     */
798    public void deleteCategory(final CmsUUID id) {
799
800        CmsDeleteWarningDialog deleteWarningDialog = new CmsDeleteWarningDialog(id) {
801
802            @Override
803            protected void onAfterDeletion() {
804
805                CmsSitemapView.getInstance().getController().loadCategories(true, null);
806            }
807        };
808        deleteWarningDialog.loadAndShow(null);
809    }
810
811    /**
812     * Disables the given model page entry within the configuration.<p>
813     *
814     * @param id the entry id
815     * @param disable <code>true</code> to disable the entry
816     */
817    public void disableModelPage(final CmsUUID id, final boolean disable) {
818
819        CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
820
821            @Override
822            public void execute() {
823
824                start(200, true);
825                getService().disableModelPage(getEntryPoint(), id, disable, this);
826
827            }
828
829            @Override
830            protected void onResponse(Void result) {
831
832                stop(false);
833                CmsSitemapView.getInstance().updateModelPageDisabledState(id, disable);
834            }
835        };
836        action.execute();
837
838    }
839
840    /**
841     * Edits the given sitemap entry.<p>
842     *
843     * @param entry the sitemap entry to update
844     * @param propertyChanges the property changes
845     * @param reloadStatus a value indicating which entries need to be reloaded after the change
846     */
847    public void edit(
848        CmsClientSitemapEntry entry,
849        List<CmsPropertyModification> propertyChanges,
850        final CmsReloadMode reloadStatus) {
851
852        CmsSitemapChange change = getChangeForEdit(entry, propertyChanges);
853        final String updateTarget = ((reloadStatus == CmsReloadMode.reloadParent))
854        ? getParentEntry(entry).getSitePath()
855        : entry.getSitePath();
856        Command callback = new Command() {
857
858            public void execute() {
859
860                if ((reloadStatus == CmsReloadMode.reloadParent) || (reloadStatus == CmsReloadMode.reloadEntry)) {
861                    updateEntry(updateTarget);
862                }
863            }
864        };
865        if (change != null) {
866            commitChange(change, callback);
867        }
868    }
869
870    /**
871     * Edits an entry and changes its URL name.<p>
872     *
873     * @param entry the entry which is being edited
874     * @param newUrlName the new URL name of the entry
875     * @param propertyChanges the property changes
876     * @param keepNewStatus <code>true</code> if the entry should keep it's new status
877     * @param reloadStatus a value indicating which entries need to be reloaded after the change
878     */
879    public void editAndChangeName(
880        final CmsClientSitemapEntry entry,
881        String newUrlName,
882        List<CmsPropertyModification> propertyChanges,
883        final boolean keepNewStatus,
884        final CmsReloadMode reloadStatus) {
885
886        CmsSitemapChange change = getChangeForEdit(entry, propertyChanges);
887        change.setName(newUrlName);
888        final CmsUUID entryId = entry.getId();
889        final boolean newStatus = keepNewStatus && entry.isNew();
890
891        final CmsUUID updateTarget = ((reloadStatus == CmsReloadMode.reloadParent))
892        ? getParentEntry(entry).getId()
893        : entryId;
894        Command callback = new Command() {
895
896            public void execute() {
897
898                if ((reloadStatus == CmsReloadMode.reloadParent) || (reloadStatus == CmsReloadMode.reloadEntry)) {
899                    updateEntry(updateTarget);
900                }
901                getEntryById(entryId).setNew(newStatus);
902            }
903
904        };
905
906        commitChange(change, callback);
907    }
908
909    /**
910     * Ensure the uniqueness of a given URL-name within the children of the given parent site-map entry.<p>
911     *
912     * @param parent the parent entry
913     * @param newName the proposed name
914     * @param callback the callback to execute
915     */
916    public void ensureUniqueName(CmsClientSitemapEntry parent, String newName, I_CmsSimpleCallback<String> callback) {
917
918        ensureUniqueName(parent.getSitePath(), newName, callback);
919    }
920
921    /**
922     * Ensure the uniqueness of a given URL-name within the children of the given parent folder.<p>
923     *
924     * @param parentFolder the parent folder
925     * @param newName the proposed name
926     * @param callback the callback to execute
927     */
928    public void ensureUniqueName(
929        final String parentFolder,
930        String newName,
931        final I_CmsSimpleCallback<String> callback) {
932
933        // using lower case folder names
934        final String lowerCaseName = newName.toLowerCase();
935        CmsRpcAction<String> action = new CmsRpcAction<String>() {
936
937            /**
938             * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
939             */
940            @Override
941            public void execute() {
942
943                start(0, false);
944                CmsCoreProvider.getService().getUniqueFileName(parentFolder, lowerCaseName, this);
945            }
946
947            /**
948             * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
949             */
950            @Override
951            protected void onResponse(String result) {
952
953                stop(false);
954                callback.execute(result);
955            }
956
957        };
958        action.execute();
959    }
960
961    /**
962     * Applies the given property modification.<p>
963     *
964     * @param propMod the property modification to apply
965     */
966    public void executePropertyModification(CmsPropertyModification propMod) {
967
968        CmsClientSitemapEntry entry = getEntryById(propMod.getId());
969        if (entry != null) {
970            Map<String, CmsClientProperty> props = getPropertiesForId(propMod.getId());
971            if (props != null) {
972                propMod.updatePropertyInMap(props);
973                entry.setOwnProperties(props);
974            }
975        }
976    }
977
978    /**
979     * Gets the category data.<p>
980     *
981     * @return the category data
982     */
983    public CmsSitemapCategoryData getCategoryData() {
984
985        return m_categoryData;
986    }
987
988    /**
989     * Retrieves the child entries of the given node from the server.<p>
990     *
991     * @param entryId the entry id
992     * @param setOpen if the entry should be opened
993     *
994     * @param callback the callback to execute after the children have been loaded
995     */
996    public void getChildren(
997        final CmsUUID entryId,
998        final boolean setOpen,
999        final AsyncCallback<CmsClientSitemapEntry> callback) {
1000
1001        getChildren(entryId, setOpen, false, callback);
1002
1003    }
1004
1005    /**
1006     * Retrieves the child entries of the given node from the server.<p>
1007     *
1008     * @param entryId the entry id
1009     * @param setOpen if the entry should be opened
1010     * @param continueIfParentNotLoaded if false, and the entry identified by the id has not been loaded, stop; else store the entry in memory
1011     *
1012     * @param callback the callback to execute after the children have been loaded
1013     */
1014    public void getChildren(
1015        final CmsUUID entryId,
1016        final boolean setOpen,
1017        final boolean continueIfParentNotLoaded,
1018        final AsyncCallback<CmsClientSitemapEntry> callback) {
1019
1020        CmsRpcAction<CmsClientSitemapEntry> getChildrenAction = new CmsRpcAction<CmsClientSitemapEntry>() {
1021
1022            /**
1023            * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
1024            */
1025            @Override
1026            public void execute() {
1027
1028                // Make the call to the sitemap service
1029                start(500, false);
1030                // loading grand children as well
1031                getService().getChildren(getEntryPoint(), entryId, 2, this);
1032            }
1033
1034            /**
1035            * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
1036            */
1037            @Override
1038            public void onResponse(CmsClientSitemapEntry result) {
1039
1040                CmsClientSitemapEntry target = getEntryById(entryId);
1041                if ((target == null) && !continueIfParentNotLoaded) {
1042                    // this might happen after an automated deletion
1043                    stop(false);
1044                    return;
1045                }
1046                CmsSitemapTreeItem item = null;
1047                if (target != null) {
1048                    target.setSubEntries(result.getSubEntries(), CmsSitemapController.this);
1049                    item = CmsSitemapTreeItem.getItemById(target.getId());
1050                    target.update(result);
1051                } else {
1052                    target = result;
1053                }
1054
1055                target.initializeAll(CmsSitemapController.this);
1056                if (item != null) {
1057
1058                    item.updateEntry(target);
1059                }
1060                m_eventBus.fireEventFromSource(new CmsSitemapLoadEvent(target, setOpen), CmsSitemapController.this);
1061                stop(false);
1062                if (callback != null) {
1063                    callback.onSuccess(result);
1064                }
1065            }
1066        };
1067        getChildrenAction.execute();
1068    }
1069
1070    /**
1071    * Returns the sitemap data.<p>
1072    *
1073    * @return the sitemap data
1074    */
1075    public CmsSitemapData getData() {
1076
1077        return m_data;
1078    }
1079
1080    /**
1081     * Returns the detail page info for a given entry id.<p>
1082     *
1083     * @param id a sitemap entry id
1084     *
1085     * @return the detail page info for that id
1086     */
1087    public CmsDetailPageInfo getDetailPageInfo(CmsUUID id) {
1088
1089        return m_allDetailPageInfos.get(id);
1090    }
1091
1092    /**
1093     * Returns the detail page table.<p>
1094     *
1095     * @return the detail page table
1096     */
1097    public CmsDetailPageTable getDetailPageTable() {
1098
1099        return m_detailPageTable;
1100    }
1101
1102    /**
1103     * Gets the effective value of a property value for a sitemap entry.<p>
1104     *
1105     * @param entry the sitemap entry
1106     * @param name the name of the property
1107     *
1108     * @return the effective value
1109     */
1110    public String getEffectiveProperty(CmsClientSitemapEntry entry, String name) {
1111
1112        CmsClientProperty prop = getEffectivePropertyObject(entry, name);
1113        if (prop == null) {
1114            return null;
1115        }
1116        return prop.getEffectiveValue();
1117    }
1118
1119    /**
1120     * Gets the value of a property which is effective at a given sitemap entry.<p>
1121     *
1122     * @param entry the sitemap entry
1123     * @param name the name of the property
1124     * @return the effective property value
1125     */
1126    public CmsClientProperty getEffectivePropertyObject(CmsClientSitemapEntry entry, String name) {
1127
1128        Map<String, CmsClientProperty> dfProps = entry.getDefaultFileProperties();
1129        CmsClientProperty result = safeLookup(dfProps, name);
1130        if (!CmsClientProperty.isPropertyEmpty(result)) {
1131            return result.withOrigin(entry.getSitePath());
1132        }
1133        result = safeLookup(entry.getOwnProperties(), name);
1134        if (!CmsClientProperty.isPropertyEmpty(result)) {
1135            return result.withOrigin(entry.getSitePath());
1136        }
1137        return getInheritedPropertyObject(entry, name);
1138    }
1139
1140    /**
1141     * Returns all entries with an id from a given list.<p>
1142     *
1143     * @param ids a list of sitemap entry ids
1144     *
1145     * @return all entries whose id is contained in the id list
1146     */
1147    public Map<CmsUUID, CmsClientSitemapEntry> getEntriesById(Collection<CmsUUID> ids) {
1148
1149        // TODO: use some map of id -> entry instead
1150        List<CmsClientSitemapEntry> entriesToProcess = new ArrayList<CmsClientSitemapEntry>();
1151        Map<CmsUUID, CmsClientSitemapEntry> result = new HashMap<CmsUUID, CmsClientSitemapEntry>();
1152
1153        entriesToProcess.add(m_data.getRoot());
1154        while (!entriesToProcess.isEmpty()) {
1155            CmsClientSitemapEntry entry = entriesToProcess.remove(entriesToProcess.size() - 1);
1156            if (ids.contains(entry.getId())) {
1157                result.put(entry.getId(), entry);
1158                if (result.size() == ids.size()) {
1159                    return result;
1160                }
1161            }
1162            entriesToProcess.addAll(entry.getSubEntries());
1163        }
1164        return result;
1165    }
1166
1167    /**
1168     * Returns the tree entry with the given path.<p>
1169     *
1170     * @param entryPath the path to look for
1171     *
1172     * @return the tree entry with the given path, or <code>null</code> if not found
1173     */
1174    public CmsClientSitemapEntry getEntry(String entryPath) {
1175
1176        return m_entriesByPath.get(entryPath);
1177    }
1178
1179    /**
1180     * Finds an entry by id.<p>
1181     *
1182     * @param id the id of the entry to find
1183     *
1184     * @return the found entry, or null if the entry wasn't found
1185     */
1186    public CmsClientSitemapEntry getEntryById(CmsUUID id) {
1187
1188        return m_entriesById.get(id);
1189    }
1190
1191    /**
1192     * Returns the gallery type with the given id.<p>
1193     *
1194     * @param typeId the type id
1195     *
1196     * @return the gallery type
1197     */
1198    public CmsGalleryType getGalleryType(Integer typeId) {
1199
1200        return m_galleryTypes.get(typeId);
1201    }
1202
1203    /**
1204     * Gets the value for a property which a sitemap entry would inherit if it didn't have its own properties.<p>
1205     *
1206     * @param entry the sitemap entry
1207     * @param name the property name
1208     * @return the inherited property value
1209     */
1210    public String getInheritedProperty(CmsClientSitemapEntry entry, String name) {
1211
1212        CmsClientProperty prop = getInheritedPropertyObject(entry, name);
1213        if (prop == null) {
1214            return null;
1215        }
1216        return prop.getEffectiveValue();
1217    }
1218
1219    /**
1220     * Gets the property object which would be inherited by a sitemap entry.<p>
1221     *
1222     * @param entry the sitemap entry
1223     * @param name the name of the property
1224     * @return the property object which would be inherited
1225     */
1226    public CmsClientProperty getInheritedPropertyObject(CmsClientSitemapEntry entry, String name) {
1227
1228        CmsClientSitemapEntry currentEntry = entry;
1229        while (currentEntry != null) {
1230            currentEntry = getParentEntry(currentEntry);
1231            if (currentEntry != null) {
1232                CmsClientProperty folderProp = currentEntry.getOwnProperties().get(name);
1233                if (!CmsClientProperty.isPropertyEmpty(folderProp)) {
1234                    return folderProp.withOrigin(currentEntry.getSitePath());
1235                }
1236            }
1237        }
1238        CmsClientProperty parentProp = getParentProperties().get(name);
1239        if (!CmsClientProperty.isPropertyEmpty(parentProp)) {
1240            String origin = parentProp.getOrigin();
1241            String siteRoot = CmsCoreProvider.get().getSiteRoot();
1242            if (origin.startsWith(siteRoot)) {
1243                origin = origin.substring(siteRoot.length());
1244            }
1245            return parentProp.withOrigin(origin);
1246        }
1247        return null;
1248
1249    }
1250
1251    /**
1252     * Returns a list of all descendant sitemap entries of a given path which have already been loaded on the client.<p>
1253     *
1254     * @param path the path for which the descendants should be collected
1255     *
1256     * @return the list of descendant sitemap entries
1257     */
1258    public List<CmsClientSitemapEntry> getLoadedDescendants(String path) {
1259
1260        LinkedList<CmsClientSitemapEntry> remainingEntries = new LinkedList<CmsClientSitemapEntry>();
1261        List<CmsClientSitemapEntry> result = new ArrayList<CmsClientSitemapEntry>();
1262        CmsClientSitemapEntry entry = getEntry(path);
1263        remainingEntries.add(entry);
1264        while (remainingEntries.size() > 0) {
1265            CmsClientSitemapEntry currentEntry = remainingEntries.removeFirst();
1266            result.add(currentEntry);
1267            for (CmsClientSitemapEntry subEntry : currentEntry.getSubEntries()) {
1268                remainingEntries.add(subEntry);
1269            }
1270        }
1271
1272        return result;
1273
1274    }
1275
1276    /**
1277     * Returns the no edit reason or <code>null</code> if editing is allowed.<p>
1278     *
1279     * @param entry the entry to get the no edit reason for
1280     *
1281     * @return the no edit reason
1282     */
1283    public String getNoEditReason(CmsClientSitemapEntry entry) {
1284
1285        return getNoEditReason(entry, true);
1286    }
1287
1288    /**
1289     * Returns the no edit reason or <code>null</code> if editing is allowed.<p>
1290     *
1291     * @param entry the entry to get the no edit reason for
1292     * @param checkChildLocks true if locks of children should be checked
1293     *
1294     * @return the no edit reason
1295     */
1296    public String getNoEditReason(CmsClientSitemapEntry entry, boolean checkChildLocks) {
1297
1298        String reason = entry.getNoEditReason();
1299        if (CmsStringUtil.isEmptyOrWhitespaceOnly(reason)) {
1300            reason = null;
1301            if ((entry.getLock() != null)
1302                && (entry.getLock().getLockOwner() != null)
1303                && !entry.getLock().isOwnedByUser()) {
1304                reason = Messages.get().key(Messages.GUI_DISABLED_LOCKED_BY_1, entry.getLock().getLockOwner());
1305
1306            }
1307            if (checkChildLocks && entry.hasBlockingLockedChildren()) {
1308                reason = Messages.get().key(Messages.GUI_DISABLED_BLOCKING_LOCKED_CHILDREN_0);
1309            }
1310        }
1311        return reason;
1312    }
1313
1314    /**
1315     * Returns the parent entry of a sitemap entry, or null if it is the root entry.<p>
1316     *
1317     * @param entry a sitemap entry
1318     *
1319     * @return the parent entry or null
1320     */
1321    public CmsClientSitemapEntry getParentEntry(CmsClientSitemapEntry entry) {
1322
1323        String path = entry.getSitePath();
1324        String parentPath = CmsResource.getParentFolder(path);
1325        if (parentPath == null) {
1326            return null;
1327        }
1328        return getEntry(parentPath);
1329    }
1330
1331    /**
1332     * Gets the properties for a given structure id.<p>
1333     *
1334     * @param id the structure id of a sitemap entry
1335     *
1336     * @return the properties for that structure id
1337     */
1338    public Map<String, CmsClientProperty> getPropertiesForId(CmsUUID id) {
1339
1340        return m_propertyMaps.get(id);
1341    }
1342
1343    /**
1344     * Returns the sitemap service instance.<p>
1345     *
1346     * @return the sitemap service instance
1347     */
1348    public I_CmsSitemapServiceAsync getService() {
1349
1350        if (m_service == null) {
1351            m_service = GWT.create(I_CmsSitemapService.class);
1352            String serviceUrl = CmsCoreProvider.get().link("org.opencms.ade.sitemap.CmsVfsSitemapService.gwt");
1353            ((ServiceDefTarget)m_service).setServiceEntryPoint(serviceUrl);
1354        }
1355        return m_service;
1356    }
1357
1358    /**
1359     * Leaves the current sitemap to open the parent sitemap.<p>
1360     */
1361    public void gotoParentSitemap() {
1362
1363        openSiteMap(getData().getParentSitemap());
1364    }
1365
1366    /**
1367     * Hides the entry within the site navigation.<p>
1368     *
1369     * Hidden entries will still be visible in the navigation mode of the sitemap editor.
1370     * They will also have a NavText and a NavPos. Only when using the NavBuilder get navigation for folder method,
1371     * they will not be included.<p>
1372     *
1373     * @param entryId the entry id
1374     */
1375    public void hideInNavigation(CmsUUID entryId) {
1376
1377        CmsClientSitemapEntry entry = getEntryById(entryId);
1378        CmsSitemapChange change = getChangeForEdit(
1379            entry,
1380            Collections.singletonList(
1381                new CmsPropertyModification(
1382                    entryId.toString()
1383                        + "/"
1384                        + CmsClientProperty.PROPERTY_NAVINFO
1385                        + "/"
1386                        + CmsClientProperty.PATH_STRUCTURE_VALUE,
1387                    CmsClientSitemapEntry.HIDDEN_NAVIGATION_ENTRY)));
1388        commitChange(change, null);
1389    }
1390
1391    /**
1392     * Checks whether this entry belongs to a detail page.<p>
1393     *
1394     * @param entry the entry to check
1395     *
1396     * @return true if this entry belongs to a detail page
1397     */
1398    public boolean isDetailPage(CmsClientSitemapEntry entry) {
1399
1400        return (entry.getDetailpageTypeName() != null) || isDetailPage(entry.getId());
1401    }
1402
1403    /**
1404     * Returns true if the id is the id of a detail page.<p>
1405     *
1406     * @param id the sitemap entry id
1407     * @return true if the id is the id of a detail page entry
1408     */
1409    public boolean isDetailPage(CmsUUID id) {
1410
1411        return m_allDetailPageInfos.containsKey(id);
1412    }
1413
1414    /**
1415     * Checks if the current sitemap is editable.<p>
1416     *
1417     * @return <code>true</code> if the current sitemap is editable
1418     */
1419    public boolean isEditable() {
1420
1421        return CmsStringUtil.isEmptyOrWhitespaceOnly(m_data.getNoEditReason());
1422    }
1423
1424    /**
1425     * Checks whether a string is the name of a hidden property.<p>
1426     *
1427     * A hidden property is a property which should not appear in the property editor
1428     * because it requires special treatment.<p>
1429     *
1430     * @param propertyName the property name which should be checked
1431     *
1432     * @return true if the argument is the name of a hidden property
1433     */
1434    public boolean isHiddenProperty(String propertyName) {
1435
1436        if (propertyName.equals("secure") && !m_data.isSecure()) {
1437            // "secure" property should not be editable in a site for which no secure server is configured
1438            return true;
1439        }
1440
1441        return m_hiddenProperties.contains(propertyName);
1442    }
1443
1444    /**
1445     * Returns true if the locale comparison mode is enabled.<p>
1446     *
1447     * @return true if the locale comparison mode is enabled
1448     */
1449    public boolean isLocaleComparisonEnabled() {
1450
1451        return m_data.isLocaleComparisonEnabled();
1452    }
1453
1454    /**
1455     * Checks if the given site path is the sitemap root.<p>
1456     *
1457     * @param sitePath the site path to check
1458     *
1459     * @return <code>true</code> if the given site path is the sitemap root
1460     */
1461    public boolean isRoot(String sitePath) {
1462
1463        return m_data.getRoot().getSitePath().equals(sitePath);
1464    }
1465
1466    /**
1467     * Ask to save the page before leaving, if necessary.<p>
1468     *
1469     * @param target the leaving target
1470     */
1471    public void leaveEditor(String target) {
1472
1473        CmsUUID baseId = getData().getRoot().getId();
1474        CmsRpcAction<String> action = new CmsRpcAction<String>() {
1475
1476            @Override
1477            public void execute() {
1478
1479                start(0, false);
1480                getService().getResourceLink(baseId, target, this);
1481            }
1482
1483            @Override
1484            protected void onResponse(String link) {
1485
1486                Window.Location.assign(link);
1487            }
1488
1489        };
1490        action.execute();
1491    }
1492
1493    /**
1494     * Loads and displays the category data.<p>
1495     *
1496     * @param openLocalCategories true if the local category tree should be opened
1497     * @param openItemId the id of the item to open
1498     */
1499    public void loadCategories(final boolean openLocalCategories, final CmsUUID openItemId) {
1500
1501        CmsRpcAction<CmsSitemapCategoryData> action = new CmsRpcAction<CmsSitemapCategoryData>() {
1502
1503            @Override
1504            public void execute() {
1505
1506                start(200, false);
1507                getService().getCategoryData(getEntryPoint(), this);
1508            }
1509
1510            @Override
1511            protected void onResponse(CmsSitemapCategoryData result) {
1512
1513                stop(false);
1514                m_categoryData = result;
1515                CmsSitemapView.getInstance().displayCategoryData(result, openLocalCategories, openItemId);
1516            }
1517        };
1518        action.execute();
1519    }
1520
1521    /**
1522     * Loads all available galleries for the current sub site.<p>
1523     */
1524    public void loadGalleries() {
1525
1526        CmsRpcAction<Map<CmsGalleryType, List<CmsGalleryFolderEntry>>> action = new CmsRpcAction<Map<CmsGalleryType, List<CmsGalleryFolderEntry>>>() {
1527
1528            @Override
1529            public void execute() {
1530
1531                start(500, false);
1532                getService().getGalleryData(m_data.getRoot().getSitePath(), this);
1533            }
1534
1535            @Override
1536            protected void onResponse(Map<CmsGalleryType, List<CmsGalleryFolderEntry>> result) {
1537
1538                storeGalleryTypes(result.keySet());
1539                CmsSitemapView.getInstance().displayGalleries(result);
1540                stop(false);
1541            }
1542        };
1543        action.execute();
1544    }
1545
1546    /**
1547     * Loads the model pages.<p>
1548     */
1549    public void loadModelPages() {
1550
1551        CmsRpcAction<CmsModelInfo> action = new CmsRpcAction<CmsModelInfo>() {
1552
1553            @Override
1554            public void execute() {
1555
1556                start(500, false);
1557                getService().getModelInfos(m_data.getRoot().getId(), this);
1558
1559            }
1560
1561            @Override
1562            protected void onResponse(CmsModelInfo result) {
1563
1564                stop(false);
1565                CmsSitemapView.getInstance().displayModelPages(result);
1566            }
1567        };
1568        action.execute();
1569    }
1570
1571    /**
1572     * Loads the new element info.<p>
1573     *
1574     * @param callback the callback to call when done
1575     */
1576    public void loadNewElementInfo(final AsyncCallback<Void> callback) {
1577
1578        CmsRpcAction<List<CmsNewResourceInfo>> newResourceInfoAction = new CmsRpcAction<List<CmsNewResourceInfo>>() {
1579
1580            @Override
1581            public void execute() {
1582
1583                start(200, true);
1584
1585                getService().getNewElementInfo(m_data.getRoot().getSitePath(), this);
1586            }
1587
1588            @Override
1589            protected void onResponse(List<CmsNewResourceInfo> result) {
1590
1591                stop(false);
1592
1593                m_data.setNewElementInfos(result);
1594                if (callback != null) {
1595                    callback.onSuccess(null);
1596                }
1597            }
1598
1599        };
1600        newResourceInfoAction.execute();
1601    }
1602
1603    /**
1604     * Loads all entries on the given path.<p>
1605     *
1606     * @param sitePath the site path
1607     */
1608    public void loadPath(final String sitePath) {
1609
1610        loadPath(sitePath, null);
1611    }
1612
1613    /**
1614     * Loads the sitemap entry for the given site path.<p>
1615     *
1616     * @param sitePath the site path
1617     * @param callback the callback
1618     */
1619    public void loadPath(final String sitePath, final AsyncCallback<CmsClientSitemapEntry> callback) {
1620
1621        loadPath(sitePath, false, callback);
1622    }
1623
1624    /**
1625     * Loads all entries on the given path.<p>
1626     *
1627     * @param sitePath the site path
1628     * @param continueIfNotLoaded parameter passed to getChildren
1629     * @param callback the callback to execute when done
1630     */
1631    public void loadPath(
1632        final String sitePath,
1633        final boolean continueIfNotLoaded,
1634        final AsyncCallback<CmsClientSitemapEntry> callback) {
1635
1636        if (getEntry(sitePath) != null) {
1637            CmsClientSitemapEntry entry = getEntry(sitePath);
1638            getChildren(entry.getId(), CmsSitemapTreeItem.getItemById(entry.getId()).isOpen(), callback);
1639        } else {
1640            String parentPath = CmsResource.getParentFolder(sitePath);
1641            CmsUUID idToLoad = null;
1642            CmsClientSitemapEntry entry = getEntry(parentPath);
1643            while (entry == null) {
1644                parentPath = CmsResource.getParentFolder(parentPath);
1645                if (parentPath == null) {
1646                    break;
1647                } else {
1648                    entry = getEntry(parentPath);
1649                }
1650            }
1651            if (entry != null) {
1652                idToLoad = entry.getId();
1653            } else {
1654                idToLoad = m_data.getSiteRootId();
1655            }
1656            CmsSitemapTreeItem treeItem = CmsSitemapTreeItem.getItemById(idToLoad);
1657            boolean open = true;
1658            if (treeItem != null) {
1659                open = treeItem.isOpen();
1660            }
1661            getChildren(idToLoad, open, continueIfNotLoaded, new AsyncCallback<CmsClientSitemapEntry>() {
1662
1663                public void onFailure(Throwable caught) {
1664
1665                    // nothing to do
1666                }
1667
1668                public void onSuccess(CmsClientSitemapEntry result) {
1669
1670                    // check if target entry is loaded
1671                    CmsClientSitemapEntry target = getEntry(sitePath);
1672                    if (target == null) {
1673                        loadPath(sitePath, continueIfNotLoaded, callback);
1674                    } else {
1675                        if (callback != null) {
1676                            callback.onSuccess(target);
1677                        }
1678                    }
1679                }
1680            });
1681        }
1682    }
1683
1684    /**
1685    * Merges a subsitemap at the given id back into this sitemap.<p>
1686    *
1687    * @param entryId the id of the sub sitemap entry
1688    */
1689    public void mergeSubSitemap(final CmsUUID entryId) {
1690
1691        CmsRpcAction<CmsSitemapChange> mergeAction = new CmsRpcAction<CmsSitemapChange>() {
1692
1693            /**
1694             * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
1695             */
1696            @Override
1697            public void execute() {
1698
1699                start(0, true);
1700                getService().mergeSubSitemap(getEntryPoint(), entryId, this);
1701            }
1702
1703            /**
1704             * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
1705             */
1706            @Override
1707            protected void onResponse(CmsSitemapChange result) {
1708
1709                stop(false);
1710                applyChange(result);
1711
1712            }
1713        };
1714        mergeAction.execute();
1715
1716    }
1717
1718    /**
1719     * Moves the given sitemap entry with all its descendants to the new position.<p>
1720     *
1721     * @param entry the sitemap entry to move
1722     * @param toPath the destination path
1723     * @param position the new position between its siblings
1724     */
1725    public void move(CmsClientSitemapEntry entry, String toPath, int position) {
1726
1727        // check for valid data
1728        if (!isValidEntryAndPath(entry, toPath)) {
1729            // invalid data, do nothing
1730            CmsDebugLog.getInstance().printLine("invalid data, doing nothing");
1731            return;
1732        }
1733        // check for relevance
1734        if (isChangedPosition(entry, toPath, position)) {
1735            // only register real changes
1736            CmsSitemapChange change = new CmsSitemapChange(entry.getId(), entry.getSitePath(), ChangeType.modify);
1737            change.setDefaultFileId(entry.getDefaultFileId());
1738            if (!toPath.equals(entry.getSitePath())) {
1739                change.setParentId(getEntry(CmsResource.getParentFolder(toPath)).getId());
1740                change.setName(CmsResource.getName(toPath));
1741            }
1742            if (CmsSitemapView.getInstance().isNavigationMode()) {
1743                change.setPosition(position);
1744            }
1745            change.setLeafType(entry.isLeafType());
1746            CmsSitemapClipboardData data = getData().getClipboardData().copy();
1747            data.addModified(entry);
1748            change.setClipBoardData(data);
1749            commitChange(change, null);
1750        }
1751    }
1752
1753    /**
1754     * Opens the property dialog for the locale comparison mode.<p>
1755     *
1756     * @param structureId the structure id of the sitemap entry to edit
1757     * @param rootId the structure id of the resource comparing to the tree root in sitemap compare mode
1758     */
1759    public void openPropertyDialogForVaadin(final CmsUUID structureId, final CmsUUID rootId) {
1760
1761        CmsRpcAction<CmsLocaleComparePropertyData> action = new CmsRpcAction<CmsLocaleComparePropertyData>() {
1762
1763            @SuppressWarnings("synthetic-access")
1764            @Override
1765            public void execute() {
1766
1767                start(0, false);
1768                m_service.loadPropertyDataForLocaleCompareView(structureId, rootId, this);
1769            }
1770
1771            @Override
1772            protected void onResponse(CmsLocaleComparePropertyData result) {
1773
1774                stop(false);
1775                CmsSitemapController.editPropertiesForLocaleCompareMode(
1776                    result,
1777                    structureId,
1778                    result.getDefaultFileId(),
1779                    null);
1780            }
1781        };
1782        action.execute();
1783
1784    }
1785
1786    /**
1787     * Opens the site-map specified.<p>
1788     *
1789     * @param sitePath the site path to the site-map folder
1790     */
1791    public void openSiteMap(String sitePath) {
1792
1793        openSiteMap(sitePath, false);
1794    }
1795
1796    /**
1797     * Opens the site-map specified.<p>
1798     *
1799     * @param sitePath the site path to the site-map folder
1800     * @param siteChange in case the site was changed
1801     */
1802    public void openSiteMap(String sitePath, boolean siteChange) {
1803
1804        String uri = CmsCoreProvider.get().link(
1805            CmsCoreProvider.get().getUri()) + "?" + CmsCoreData.PARAM_PATH + "=" + sitePath;
1806        if (!siteChange) {
1807            uri += "&" + CmsCoreData.PARAM_RETURNCODE + "=" + getData().getReturnCode();
1808        }
1809        Window.Location.replace(uri);
1810    }
1811
1812    /**
1813     * Recomputes properties for all sitemap entries.<p>
1814     */
1815    public void recomputeProperties() {
1816
1817        CmsClientSitemapEntry root = getData().getRoot();
1818        recomputeProperties(root);
1819    }
1820
1821    /**
1822     * Refreshes the root entry.<p>
1823     *
1824     * @param callback the callback to call after the entry has been refreshed
1825     */
1826    public void refreshRoot(AsyncCallback<Void> callback) {
1827
1828        updateEntry(getData().getRoot().getId(), callback);
1829    }
1830
1831    /**
1832     * @see org.opencms.ade.sitemap.shared.I_CmsSitemapController#registerEntry(org.opencms.ade.sitemap.shared.CmsClientSitemapEntry)
1833     */
1834    public void registerEntry(CmsClientSitemapEntry entry) {
1835
1836        if (m_entriesById.containsKey(entry.getId())) {
1837            CmsClientSitemapEntry oldEntry = m_entriesById.get(entry.getId());
1838            oldEntry.update(entry);
1839            if (oldEntry != m_entriesByPath.get(oldEntry.getSitePath())) {
1840                m_entriesByPath.put(oldEntry.getSitePath(), oldEntry);
1841            }
1842        } else {
1843            m_entriesById.put(entry.getId(), entry);
1844            m_entriesByPath.put(entry.getSitePath(), entry);
1845        }
1846
1847    }
1848
1849    /**
1850     * @see org.opencms.ade.sitemap.shared.I_CmsSitemapController#registerPathChange(org.opencms.ade.sitemap.shared.CmsClientSitemapEntry, java.lang.String)
1851     */
1852    public void registerPathChange(CmsClientSitemapEntry entry, String oldPath) {
1853
1854        m_entriesById.put(entry.getId(), entry);
1855        m_entriesByPath.remove(oldPath);
1856        m_entriesByPath.put(entry.getSitePath(), entry);
1857    }
1858
1859    /**
1860     * Removes the entry with the given site-path from navigation.<p>
1861     *
1862     * @param entryId the entry id
1863     */
1864    public void removeFromNavigation(CmsUUID entryId) {
1865
1866        CmsClientSitemapEntry entry = getEntryById(entryId);
1867        CmsClientSitemapEntry parent = getEntry(CmsResource.getParentFolder(entry.getSitePath()));
1868        CmsSitemapChange change = new CmsSitemapChange(entry.getId(), entry.getSitePath(), ChangeType.remove);
1869        change.setParentId(parent.getId());
1870        change.setDefaultFileId(entry.getDefaultFileId());
1871        CmsSitemapClipboardData data = CmsSitemapView.getInstance().getController().getData().getClipboardData().copy();
1872        data.addModified(entry);
1873        change.setClipBoardData(data);
1874        //TODO: handle detail page delete
1875
1876        commitChange(change, null);
1877    }
1878
1879    /**
1880     * Removes a model page from the sitemap's configuration.<p>
1881     *
1882     * @param id the structure id of the model page
1883     *
1884     * @param asyncCallback the callback to call when done
1885     */
1886    public void removeModelPage(final CmsUUID id, final AsyncCallback<Void> asyncCallback) {
1887
1888        CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
1889
1890            @Override
1891            public void execute() {
1892
1893                start(200, true);
1894                getService().removeModelPage(getEntryPoint(), id, this);
1895
1896            }
1897
1898            @Override
1899            protected void onResponse(Void result) {
1900
1901                stop(false);
1902                loadNewElementInfo(null);
1903                asyncCallback.onSuccess(null);
1904
1905            }
1906        };
1907        action.execute();
1908
1909    }
1910
1911    /**
1912     * @see org.opencms.ade.sitemap.shared.I_CmsSitemapController#replaceProperties(org.opencms.util.CmsUUID, java.util.Map)
1913     */
1914    public Map<String, CmsClientProperty> replaceProperties(CmsUUID id, Map<String, CmsClientProperty> properties) {
1915
1916        if ((id == null) || (properties == null)) {
1917            return null;
1918        }
1919        Map<String, CmsClientProperty> props = m_propertyMaps.get(id);
1920        if (props == null) {
1921            props = properties;
1922            m_propertyMaps.put(id, props);
1923        } else {
1924            props.clear();
1925            props.putAll(properties);
1926        }
1927        return props;
1928
1929    }
1930
1931    /**
1932     * Sets the editor mode in the user session.<p>
1933     *
1934     * @param editorMode the editor mode
1935     */
1936    public void setEditorModeInSession(final EditorMode editorMode) {
1937
1938        CmsRpcAction<Void> action = new CmsRpcAction<Void>() {
1939
1940            @Override
1941            public void execute() {
1942
1943                getService().setEditorMode(editorMode, this);
1944            }
1945
1946            @Override
1947            protected void onResponse(Void result) {
1948
1949                // nothing to do
1950
1951            }
1952        };
1953        action.execute();
1954    }
1955
1956    /**
1957     * Shows a formerly hidden entry in the navigation.<p>
1958     *
1959     * @see #hideInNavigation(CmsUUID)
1960     *
1961     * @param entryId the entry id
1962     */
1963    public void showInNavigation(CmsUUID entryId) {
1964
1965        CmsClientSitemapEntry entry = getEntryById(entryId);
1966        CmsSitemapChange change = getChangeForEdit(
1967            entry,
1968            Collections.singletonList(
1969                new CmsPropertyModification(
1970                    entryId.toString()
1971                        + "/"
1972                        + CmsClientProperty.PROPERTY_NAVINFO
1973                        + "/"
1974                        + CmsClientProperty.PATH_STRUCTURE_VALUE,
1975                    "")));
1976        commitChange(change, null);
1977    }
1978
1979    /**
1980     * Undeletes the resource with the given structure id.<p>
1981     *
1982     * @param entryId the entry id
1983     * @param sitePath the site-path
1984     */
1985    public void undelete(CmsUUID entryId, String sitePath) {
1986
1987        CmsSitemapChange change = new CmsSitemapChange(entryId, sitePath, ChangeType.undelete);
1988        CmsSitemapClipboardData data = CmsSitemapView.getInstance().getController().getData().getClipboardData().copy();
1989        data.getDeletions().remove(entryId);
1990        change.setClipBoardData(data);
1991        commitChange(change, null);
1992    }
1993
1994    /**
1995     * Updates the given entry.<p>
1996     *
1997     * @param entryId the entry id
1998      */
1999    public void updateEntry(CmsUUID entryId) {
2000
2001        getChildren(entryId, CmsSitemapTreeItem.getItemById(entryId).isOpen(), null);
2002    }
2003
2004    /**
2005     * Updates the given entry.<p>
2006     *
2007     * @param entryId the entry id
2008     * @param callback the callback to call after the entry has been updated
2009      */
2010    public void updateEntry(CmsUUID entryId, final AsyncCallback<Void> callback) {
2011
2012        getChildren(
2013            entryId,
2014            CmsSitemapTreeItem.getItemById(entryId).isOpen(),
2015            new AsyncCallback<CmsClientSitemapEntry>() {
2016
2017                public void onFailure(Throwable caught) {
2018
2019                    // nothing to do
2020
2021                }
2022
2023                public void onSuccess(CmsClientSitemapEntry result) {
2024
2025                    if (callback != null) {
2026                        callback.onSuccess(null);
2027                    }
2028                }
2029            });
2030    }
2031
2032    /**
2033    * Updates the given entry.<p>
2034    *
2035    * @param sitePath the entry sitepath
2036     */
2037    public void updateEntry(String sitePath) {
2038
2039        CmsClientSitemapEntry entry = getEntry(sitePath);
2040        if ((entry != null) && (CmsSitemapTreeItem.getItemById(entry.getId()) != null)) {
2041            getChildren(entry.getId(), CmsSitemapTreeItem.getItemById(entry.getId()).isOpen(), null);
2042        }
2043    }
2044
2045    /**
2046     * Updates the given entry only, not evaluating any child changes.<p>
2047     *
2048     * @param entryId the entry id
2049     */
2050    public void updateSingleEntry(final CmsUUID entryId) {
2051
2052        CmsRpcAction<CmsClientSitemapEntry> getChildrenAction = new CmsRpcAction<CmsClientSitemapEntry>() {
2053
2054            /**
2055            * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
2056            */
2057            @Override
2058            public void execute() {
2059
2060                getService().getChildren(getEntryPoint(), entryId, 0, this);
2061            }
2062
2063            /**
2064            * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
2065            */
2066            @Override
2067            public void onResponse(CmsClientSitemapEntry result) {
2068
2069                CmsClientSitemapEntry target = getEntryById(entryId);
2070                if (target == null) {
2071                    // this might happen after an automated deletion
2072                    stop(false);
2073                    return;
2074                }
2075                target.update(result);
2076                CmsSitemapTreeItem item = CmsSitemapTreeItem.getItemById(target.getId());
2077                item.updateEntry(target);
2078            }
2079        };
2080        getChildrenAction.execute();
2081    }
2082
2083    /**
2084     * Fires a sitemap change event.<p>
2085     *
2086     * @param change the change event to fire
2087     */
2088    protected void applyChange(CmsSitemapChange change) {
2089
2090        // update the clip board data
2091        if (change.getClipBoardData() != null) {
2092            getData().getClipboardData().setDeletions(change.getClipBoardData().getDeletions());
2093            getData().getClipboardData().setModifications(change.getClipBoardData().getModifications());
2094        }
2095        switch (change.getChangeType()) {
2096            case bumpDetailPage:
2097                CmsDetailPageTable detailPageTable = getData().getDetailPageTable();
2098                if (detailPageTable.contains(change.getEntryId())) {
2099                    detailPageTable.makeDefault(change.getEntryId());
2100                }
2101                break;
2102            case clipboardOnly:
2103                // nothing to do
2104                break;
2105            case remove:
2106                CmsClientSitemapEntry entry = getEntryById(change.getEntryId());
2107                entry.setInNavigation(false);
2108                CmsPropertyModification propMod = new CmsPropertyModification(
2109                    entry.getId(),
2110                    CmsClientProperty.PROPERTY_NAVTEXT,
2111                    null,
2112                    true);
2113                executePropertyModification(propMod);
2114                propMod = new CmsPropertyModification(entry.getId(), CmsClientProperty.PROPERTY_NAVTEXT, null, true);
2115                executePropertyModification(propMod);
2116                entry.normalizeProperties();
2117                break;
2118            case undelete:
2119                updateEntry(change.getParentId());
2120                break;
2121            case create:
2122                CmsClientSitemapEntry newEntry = change.getUpdatedEntry();
2123                getEntryById(change.getParentId()).insertSubEntry(newEntry, change.getPosition(), this);
2124                newEntry.initializeAll(this);
2125                if (isDetailPage(newEntry)) {
2126                    CmsDetailPageInfo info = getDetailPageInfo(newEntry.getId());
2127                    if (info == null) {
2128                        info = new CmsDetailPageInfo(
2129                            newEntry.getId(),
2130                            newEntry.getSitePath(),
2131                            newEntry.getDetailpageTypeName(),
2132                            /* qualifier = */null,
2133                            new ArrayList<>(),
2134                            newEntry.getVfsModeIcon());
2135                    }
2136                    addDetailPageInfo(info);
2137                }
2138                break;
2139            case delete:
2140                removeEntry(change.getEntryId(), change.getParentId());
2141                break;
2142            case modify:
2143                if (change.hasNewParent() || change.hasChangedPosition()) {
2144                    CmsClientSitemapEntry moved = getEntryById(change.getEntryId());
2145                    String oldSitepath = moved.getSitePath();
2146                    CmsClientSitemapEntry sourceParent = getEntry(CmsResource.getParentFolder(oldSitepath));
2147                    sourceParent.removeSubEntry(moved.getId());
2148                    CmsClientSitemapEntry destParent = change.hasNewParent()
2149                    ? getEntryById(change.getParentId())
2150                    : sourceParent;
2151                    if (change.getPosition() < destParent.getSubEntries().size()) {
2152                        destParent.insertSubEntry(moved, change.getPosition(), this);
2153                    } else {
2154                        // inserting as last entry of the parent list
2155                        destParent.addSubEntry(moved, this);
2156                    }
2157                    if (change.hasNewParent()) {
2158                        cleanupOldPaths(oldSitepath, destParent.getSitePath());
2159                    }
2160                }
2161                if (change.hasChangedName()) {
2162                    CmsClientSitemapEntry changed = getEntryById(change.getEntryId());
2163                    String oldSitepath = changed.getSitePath();
2164                    String parentPath = CmsResource.getParentFolder(oldSitepath);
2165                    String newSitepath = CmsStringUtil.joinPaths(parentPath, change.getName());
2166                    changed.updateSitePath(newSitepath, this);
2167                    cleanupOldPaths(oldSitepath, newSitepath);
2168                }
2169                if (change.hasChangedProperties()) {
2170                    for (CmsPropertyModification modification : change.getPropertyChanges()) {
2171                        executePropertyModification(modification);
2172                    }
2173                    getEntryById(change.getEntryId()).normalizeProperties();
2174                }
2175                if (change.getUpdatedEntry() != null) {
2176                    CmsClientSitemapEntry oldEntry = getEntryById(change.getEntryId());
2177                    CmsClientSitemapEntry parent = getEntry(CmsResource.getParentFolder(oldEntry.getSitePath()));
2178                    removeEntry(change.getEntryId(), parent.getId());
2179                    parent.insertSubEntry(change.getUpdatedEntry(), oldEntry.getPosition(), this);
2180                    change.getUpdatedEntry().initializeAll(this);
2181                }
2182                break;
2183            default:
2184        }
2185        if (change.getChangeType() != ChangeType.delete) {
2186            recomputeProperties();
2187        }
2188        m_eventBus.fireEventFromSource(new CmsSitemapChangeEvent(change), this);
2189    }
2190
2191    /**
2192    * Adds a change to the queue.<p>
2193    *
2194    * @param change the change to commit
2195    * @param callback the callback to execute after the change has been applied
2196    */
2197    protected void commitChange(final CmsSitemapChange change, final Command callback) {
2198
2199        if (change != null) {
2200            // save the sitemap
2201            CmsRpcAction<CmsSitemapChange> saveAction = new CmsRpcAction<CmsSitemapChange>() {
2202
2203                /**
2204                * @see org.opencms.gwt.client.rpc.CmsRpcAction#execute()
2205                */
2206                @Override
2207                public void execute() {
2208
2209                    start(0, true);
2210                    getService().save(getEntryPoint(), change, this);
2211
2212                }
2213
2214                @Override
2215                public void onFailure(Throwable t) {
2216
2217                    // An error after a drag/drop operation can cause click events to get sent to the sitemap entry's move handle
2218                    // instead of the error dialog's buttons. We use releaseCapture() to fix this.
2219                    DOM.releaseCapture(DOM.getCaptureElement());
2220                    super.onFailure(t);
2221                }
2222
2223                /**
2224                * @see org.opencms.gwt.client.rpc.CmsRpcAction#onResponse(java.lang.Object)
2225                */
2226                @Override
2227                public void onResponse(CmsSitemapChange result) {
2228
2229                    stop(false);
2230
2231                    applyChange(result);
2232                    if (callback != null) {
2233                        callback.execute();
2234                    }
2235                }
2236            };
2237            saveAction.execute();
2238        }
2239    }
2240
2241    /**
2242     * Creates a change object for an edit operation.<p>
2243     *
2244     * @param entry the edited sitemap entry
2245     * @param propertyChanges the list of property changes
2246     *
2247     * @return the change object
2248     */
2249    protected CmsSitemapChange getChangeForEdit(
2250        CmsClientSitemapEntry entry,
2251        List<CmsPropertyModification> propertyChanges) {
2252
2253        CmsSitemapChange change = new CmsSitemapChange(entry.getId(), entry.getSitePath(), ChangeType.modify);
2254        change.setDefaultFileId(entry.getDefaultFileId());
2255        change.setLeafType(entry.isLeafType());
2256        List<CmsPropertyModification> propertyChangeData = new ArrayList<CmsPropertyModification>();
2257        for (CmsPropertyModification propChange : propertyChanges) {
2258            propertyChangeData.add(propChange);
2259        }
2260        change.setPropertyChanges(propertyChangeData);
2261
2262        CmsSitemapClipboardData data = getData().getClipboardData().copy();
2263        data.addModified(entry);
2264        change.setClipBoardData(data);
2265        return change;
2266    }
2267
2268    /**
2269     * Returns the URI of the current sitemap.<p>
2270     *
2271     * @return the URI of the current sitemap
2272     */
2273    protected String getEntryPoint() {
2274
2275        return m_data.getRoot().getSitePath();
2276
2277    }
2278
2279    /**
2280     * Helper method for getting the full path of a sitemap entry whose URL name is being edited.<p>
2281     *
2282     * @param entry the sitemap entry
2283     * @param newUrlName the new url name of the sitemap entry
2284     *
2285     * @return the new full site path of the sitemap entry
2286     */
2287    protected String getPath(CmsClientSitemapEntry entry, String newUrlName) {
2288
2289        if (newUrlName.equals("")) {
2290            return entry.getSitePath();
2291        }
2292        return CmsResource.getParentFolder(entry.getSitePath()) + newUrlName + "/";
2293    }
2294
2295    /**
2296     * Returns the sitemap service instance.<p>
2297     *
2298     * @return the sitemap service instance
2299     */
2300    protected I_CmsVfsServiceAsync getVfsService() {
2301
2302        if (m_vfsService == null) {
2303            m_vfsService = CmsCoreProvider.getVfsService();
2304        }
2305        return m_vfsService;
2306    }
2307
2308    /**
2309     * Initializes the detail page information.<p>
2310     */
2311    protected void initDetailPageInfos() {
2312
2313        for (CmsUUID id : m_data.getDetailPageTable().getAllIds()) {
2314            CmsDetailPageInfo info = m_data.getDetailPageTable().get(id);
2315            m_allDetailPageInfos.put(id, info);
2316        }
2317    }
2318
2319    /**
2320     * Creates a new client sitemap entry bean to use for the RPC call which actually creates the entry on the server side.<p>
2321     *
2322     * @param parent the parent entry
2323     * @param callback the callback to execute
2324     */
2325    protected void makeNewEntry(
2326        final CmsClientSitemapEntry parent,
2327        final I_CmsSimpleCallback<CmsClientSitemapEntry> callback) {
2328
2329        ensureUniqueName(parent, NEW_ENTRY_NAME, new I_CmsSimpleCallback<String>() {
2330
2331            public void execute(String urlName) {
2332
2333                CmsClientSitemapEntry newEntry = new CmsClientSitemapEntry();
2334                //newEntry.setTitle(urlName);
2335                newEntry.setName(urlName);
2336                String sitePath = parent.getSitePath() + urlName + "/";
2337                newEntry.setSitePath(sitePath);
2338                newEntry.setVfsPath(null);
2339                newEntry.setPosition(0);
2340                newEntry.setNew(true);
2341                newEntry.setInNavigation(true);
2342                newEntry.setResourceTypeName("folder");
2343                newEntry.getOwnProperties().put(
2344                    CmsClientProperty.PROPERTY_TITLE,
2345                    new CmsClientProperty(CmsClientProperty.PROPERTY_TITLE, NEW_ENTRY_NAME, NEW_ENTRY_NAME));
2346                callback.execute(newEntry);
2347            }
2348        });
2349
2350    }
2351
2352    /**
2353     * Recomputes the properties for a client sitemap entry.<p>
2354     *
2355     * @param entry the entry for whose descendants the properties should be recomputed
2356     */
2357    protected void recomputeProperties(CmsClientSitemapEntry entry) {
2358
2359        for (I_CmsPropertyUpdateHandler handler : m_propertyUpdateHandlers) {
2360            handler.handlePropertyUpdate(entry);
2361        }
2362        for (CmsClientSitemapEntry child : entry.getSubEntries()) {
2363            recomputeProperties(child);
2364        }
2365    }
2366
2367    /**
2368     * Store the gallery type information.<p>
2369     *
2370     * @param galleryTypes the gallery types
2371     */
2372    void storeGalleryTypes(Collection<CmsGalleryType> galleryTypes) {
2373
2374        m_galleryTypes.clear();
2375        for (CmsGalleryType type : galleryTypes) {
2376            m_galleryTypes.put(Integer.valueOf(type.getTypeId()), type);
2377        }
2378    }
2379
2380    /**
2381     * Cleans up wrong path references.<p>
2382     *
2383     * @param oldSitepath the old sitepath
2384     * @param newSitepath the new sitepath
2385     */
2386    private void cleanupOldPaths(String oldSitepath, String newSitepath) {
2387
2388        // use a separate list to avoid concurrent changes
2389        List<CmsClientSitemapEntry> entries = new ArrayList<CmsClientSitemapEntry>(m_entriesById.values());
2390        for (CmsClientSitemapEntry entry : entries) {
2391            if (entry.getSitePath().startsWith(oldSitepath)) {
2392                String currentPath = entry.getSitePath();
2393                String partAfterOldSitePath = currentPath.substring(oldSitepath.length());
2394
2395                String updatedSitePath = "".equals(partAfterOldSitePath)
2396                ? newSitepath
2397                : CmsStringUtil.joinPaths(newSitepath, partAfterOldSitePath);
2398                entry.updateSitePath(updatedSitePath, this);
2399            }
2400        }
2401    }
2402
2403    /**
2404     * Returns the properties above the sitemap root.<p>
2405     *
2406     * @return the map of properties of the root's parent
2407     */
2408    private Map<String, CmsClientProperty> getParentProperties() {
2409
2410        return m_data.getParentProperties();
2411    }
2412
2413    /**
2414     * Checks if the given path and position indicate a changed position for the entry.<p>
2415     *
2416     * @param entry the sitemap entry to move
2417     * @param toPath the destination path
2418     * @param position the new position between its siblings
2419     *
2420     * @return <code>true</code> if this is a position change
2421     */
2422    private boolean isChangedPosition(CmsClientSitemapEntry entry, String toPath, int position) {
2423
2424        return (!entry.getSitePath().equals(toPath) || (entry.getPosition() != position));
2425    }
2426
2427    /**
2428     * Validates the entry and the given path.<p>
2429     *
2430     * @param entry the entry
2431     * @param toPath the path
2432     *
2433     * @return <code>true</code> if entry and path are valid
2434     */
2435    private boolean isValidEntryAndPath(CmsClientSitemapEntry entry, String toPath) {
2436
2437        return ((toPath != null)
2438            && (CmsResource.getParentFolder(toPath) != null)
2439            && (entry != null)
2440            && (getEntry(CmsResource.getParentFolder(toPath)) != null)
2441            && (getEntry(entry.getSitePath()) != null));
2442    }
2443
2444    /**
2445     * Removes all children of the given entry recursively from the data model.<p>
2446     *
2447     * @param entry the entry
2448     */
2449    private void removeAllChildren(CmsClientSitemapEntry entry) {
2450
2451        for (CmsClientSitemapEntry child : entry.getSubEntries()) {
2452            removeAllChildren(child);
2453            m_entriesById.remove(child.getId());
2454            m_entriesByPath.remove(child.getSitePath());
2455            // apply to detailpage table
2456            CmsDetailPageTable detailPageTable = getData().getDetailPageTable();
2457            if (detailPageTable.contains(child.getId())) {
2458                detailPageTable.remove(child.getId());
2459            }
2460        }
2461        entry.getSubEntries().clear();
2462    }
2463
2464    /**
2465     * Removes the given entry and it's descendants from the modified list.<p>
2466     *
2467     * @param entry the entry
2468     * @param data the clip board data
2469     */
2470    private void removeDeletedFromModified(CmsClientSitemapEntry entry, CmsSitemapClipboardData data) {
2471
2472        data.removeModified(entry);
2473        for (CmsClientSitemapEntry child : entry.getSubEntries()) {
2474            removeDeletedFromModified(child, data);
2475        }
2476    }
2477
2478    /**
2479     * Removes the given entry from the data model.<p>
2480     *
2481     * @param entryId the id of the entry to remove
2482     * @param parentId the parent entry id
2483     */
2484    private void removeEntry(CmsUUID entryId, CmsUUID parentId) {
2485
2486        CmsClientSitemapEntry entry = getEntryById(entryId);
2487        CmsClientSitemapEntry deleteParent = getEntryById(parentId);
2488        removeAllChildren(entry);
2489        deleteParent.removeSubEntry(entry.getPosition());
2490        m_entriesById.remove(entryId);
2491        m_entriesByPath.remove(entry.getSitePath());
2492        // apply to detailpage table
2493        CmsDetailPageTable detailPageTable = getData().getDetailPageTable();
2494        if (detailPageTable.contains(entryId)) {
2495            detailPageTable.remove(entryId);
2496        }
2497    }
2498}