001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (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;
029
030import static org.opencms.ade.sitemap.shared.I_CmsAliasConstants.PARAM_IMPORTFILE;
031import static org.opencms.ade.sitemap.shared.I_CmsAliasConstants.PARAM_SEPARATOR;
032import static org.opencms.ade.sitemap.shared.I_CmsAliasConstants.PARAM_SITEROOT;
033
034import org.opencms.db.CmsAlias;
035import org.opencms.db.CmsAliasManager;
036import org.opencms.db.CmsRewriteAlias;
037import org.opencms.file.CmsObject;
038import org.opencms.file.CmsResource;
039import org.opencms.file.CmsResourceFilter;
040import org.opencms.gwt.shared.alias.CmsAliasEditValidationReply;
041import org.opencms.gwt.shared.alias.CmsAliasEditValidationRequest;
042import org.opencms.gwt.shared.alias.CmsAliasImportResult;
043import org.opencms.gwt.shared.alias.CmsAliasSaveValidationRequest;
044import org.opencms.gwt.shared.alias.CmsAliasTableRow;
045import org.opencms.gwt.shared.alias.CmsRewriteAliasTableRow;
046import org.opencms.main.CmsException;
047import org.opencms.main.OpenCms;
048import org.opencms.util.CmsUUID;
049
050import java.util.ArrayList;
051import java.util.HashSet;
052import java.util.List;
053import java.util.Locale;
054import java.util.Set;
055
056import org.apache.commons.fileupload2.core.DiskFileItem;
057import org.apache.commons.fileupload2.core.DiskFileItemFactory;
058import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletDiskFileUpload;
059
060import com.google.common.collect.Sets;
061
062import jakarta.servlet.http.HttpServletRequest;
063import jakarta.servlet.http.HttpServletResponse;
064
065/**
066 * Helper class used by a service to edit or import aliases for a whole site.<p>
067 */
068public class CmsAliasBulkEditHelper {
069
070    /** The current CMS context. */
071    private CmsObject m_cms;
072
073    /** Flag to indicate whether the validation was successful. */
074    private boolean m_hasErrors;
075
076    /**
077     * Creates a new helper object.<p>
078     *
079     * @param cms the current CMS context
080     */
081    public CmsAliasBulkEditHelper(CmsObject cms) {
082
083        m_cms = cms;
084    }
085
086    /**
087     * Imports uploaded aliases from a request.<p>
088     *
089     * @param request the request containing the uploaded aliases
090     * @param response the response
091     * @throws Exception if something goes wrong
092     */
093    public void importAliases(HttpServletRequest request, HttpServletResponse response) throws Exception {
094
095        DiskFileItemFactory factory = DiskFileItemFactory.builder().get();
096        JakartaServletDiskFileUpload upload = new JakartaServletDiskFileUpload(factory);
097        @SuppressWarnings("unchecked")
098        List<DiskFileItem> items = upload.parseRequest(request);
099        byte[] data = null;
100        String siteRoot = null;
101        String separator = ",";
102        for (DiskFileItem fileItem : items) {
103            String name = fileItem.getFieldName();
104            if (PARAM_IMPORTFILE.equals(name)) {
105                data = fileItem.get();
106            } else if (PARAM_SITEROOT.equals(name)) {
107                siteRoot = new String(fileItem.get(), request.getCharacterEncoding());
108            } else if (PARAM_SEPARATOR.equals(name)) {
109                separator = new String(fileItem.get(), request.getCharacterEncoding());
110            }
111        }
112        List<CmsAliasImportResult> result = new ArrayList<CmsAliasImportResult>();
113        if ((siteRoot != null) && (data != null)) {
114            result = OpenCms.getAliasManager().importAliases(m_cms, data, siteRoot, separator);
115        }
116        String key = CmsVfsSitemapService.addAliasImportResult(result);
117        // only respond with a key, then the client can get the data for the key via GWT-RPC
118        response.getWriter().print(key);
119    }
120
121    /**
122     * Saves alias changes to the database.<p>
123     *
124     * @param saveRequest an object containing the alias changes to save
125     *
126     * @return a validation error if the alias data is invalid, or null otherwise
127     *
128     * @throws CmsException if something goes wrong
129     */
130    public CmsAliasEditValidationReply saveAliases(CmsAliasSaveValidationRequest saveRequest) throws CmsException {
131
132        CmsAliasEditValidationReply reply = validateAliases(saveRequest);
133        if (m_hasErrors) {
134            return reply;
135        } else {
136            List<CmsRewriteAliasTableRow> rewriteData = saveRequest.getRewriteData();
137            OpenCms.getAliasManager().saveRewriteAliases(
138                m_cms,
139                m_cms.getRequestContext().getSiteRoot(),
140                convertRewriteData(rewriteData));
141            Set<CmsUUID> allTouchedIds = new HashSet<CmsUUID>();
142            List<CmsAliasTableRow> rows = saveRequest.getEditedData();
143            for (CmsAliasTableRow row : rows) {
144                if (row.isEdited()) {
145                    allTouchedIds.add(row.getStructureId());
146                    if (row.getOriginalStructureId() != null) {
147                        allTouchedIds.add(row.getOriginalStructureId());
148                    }
149                }
150            }
151            allTouchedIds.addAll(saveRequest.getDeletedIds());
152            CmsAliasManager aliasManager = OpenCms.getAliasManager();
153            String siteRoot = saveRequest.getSiteRoot();
154            List<CmsAlias> aliases = aliasManager.getAliasesForSite(m_cms, siteRoot);
155            Set<CmsAlias> aliasSet = new HashSet<CmsAlias>();
156            Set<CmsAlias> editedAliasSet = new HashSet<CmsAlias>();
157            aliasSet.addAll(aliases);
158            for (CmsAliasTableRow row : rows) {
159                CmsAlias editedAlias = new CmsAlias(row.getStructureId(), siteRoot, row.getAliasPath(), row.getMode());
160                editedAliasSet.add(editedAlias);
161            }
162            Set<CmsAlias> toDelete = Sets.difference(aliasSet, editedAliasSet);
163            toDelete = filterStructureId(toDelete, allTouchedIds);
164            Set<CmsAlias> toAdd = Sets.difference(editedAliasSet, aliasSet);
165            toAdd = filterStructureId(toAdd, allTouchedIds);
166
167            aliasManager.updateAliases(m_cms, toDelete, toAdd);
168            return null;
169        }
170    }
171
172    /**
173     * Validates the alias data.<p>
174     *
175     * @param validationRequest an object containing the alias data to validate
176     *
177     * @return the validation result
178     */
179    public CmsAliasEditValidationReply validateAliases(CmsAliasEditValidationRequest validationRequest) {
180
181        List<CmsAliasTableRow> editedData = validationRequest.getEditedData();
182        CmsAliasTableRow newEntry = validationRequest.getNewEntry();
183        if (newEntry != null) {
184            newEntry.setKey((new CmsUUID()).toString());
185            editedData.add(newEntry);
186
187        }
188        CmsObject cms = m_cms;
189        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
190        for (CmsAliasTableRow row : editedData) {
191            row.clearErrors();
192            validateSingleAliasRow(cms, row);
193        }
194        Set<String> foundAliasPaths = new HashSet<String>();
195        Set<String> duplicateAliasPaths = new HashSet<String>();
196        for (CmsAliasTableRow row : editedData) {
197            String aliasPath = row.getAliasPath();
198            if (foundAliasPaths.contains(aliasPath)) {
199                duplicateAliasPaths.add(aliasPath);
200            }
201            foundAliasPaths.add(aliasPath);
202        }
203        for (CmsAliasTableRow row : editedData) {
204            if (duplicateAliasPaths.contains(row.getAliasPath())) {
205                if (row.getPathError() == null) {
206                    row.setAliasError(messageDuplicateAliasPath(locale));
207                    m_hasErrors = true;
208                }
209            }
210        }
211        CmsAliasEditValidationReply result = new CmsAliasEditValidationReply();
212        List<CmsAliasTableRow> changedRows = new ArrayList<CmsAliasTableRow>();
213        for (CmsAliasTableRow row : editedData) {
214            if (row.isChanged()) {
215                changedRows.add(row);
216            }
217        }
218        if (newEntry != null) {
219            changedRows.remove(newEntry);
220        }
221        result.setChangedRows(changedRows);
222        result.setValidatedNewEntry(newEntry);
223        return result;
224    }
225
226    /**
227     * Filters all aliases from a set whose structure id is in a given set of structure ids.<p>
228     *
229     * @param aliases the aliases to filter
230     * @param structureIds the structure ids for which we want the aliases
231     *
232     * @return the filtered structure ids
233     */
234    protected Set<CmsAlias> filterStructureId(Set<CmsAlias> aliases, Set<CmsUUID> structureIds) {
235
236        Set<CmsAlias> result = new HashSet<CmsAlias>();
237        for (CmsAlias alias : aliases) {
238            if (structureIds.contains(alias.getStructureId())) {
239                result.add(alias);
240            }
241        }
242        return result;
243    }
244
245    /**
246     * Converts rewrite alias table rows to rewrite alias objects.<p>
247     *
248     * @param rewriteData the rewrite data
249     *
250     * @return the converted rewrite aliases
251     */
252    private List<CmsRewriteAlias> convertRewriteData(List<CmsRewriteAliasTableRow> rewriteData) {
253
254        String siteRoot = m_cms.getRequestContext().getSiteRoot();
255        List<CmsRewriteAlias> result = new ArrayList<CmsRewriteAlias>();
256        for (CmsRewriteAliasTableRow row : rewriteData) {
257            CmsRewriteAlias alias = new CmsRewriteAlias(
258                row.getId(),
259                siteRoot,
260                row.getPatternString(),
261                row.getReplacementString(),
262                row.getMode());
263            result.add(alias);
264        }
265        return result;
266    }
267
268    /**
269     * Message accessor.
270     *
271     * @param locale the locale for messages
272     *
273     * @return the message string
274     */
275    private String messageDuplicateAliasPath(Locale locale) {
276
277        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_DUPLICATE_ALIAS_PATH_0);
278    }
279
280    /**
281     * Message accessor.
282     *
283     * @param locale the locale for messages
284     *
285     * @return the message string
286     */
287    private String messageInvalidAliasPath(Locale locale) {
288
289        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_INVALID_ALIAS_PATH_0);
290    }
291
292    /**
293     * Message accessor.
294     *
295     * @param locale the locale for messages
296     *
297     * @return the message string
298     */
299    private String messageResourceNotFound(Locale locale) {
300
301        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_RESOURCE_NOT_FOUND_0);
302
303    }
304
305    /**
306     * Validates a single alias row.<p>
307     *
308     * @param cms the current CMS context
309     * @param row the row to validate
310     */
311    private void validateSingleAliasRow(CmsObject cms, CmsAliasTableRow row) {
312
313        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
314        if (row.getStructureId() == null) {
315            String path = row.getResourcePath();
316            try {
317                CmsResource resource = cms.readResource(path, CmsResourceFilter.ALL);
318                row.setStructureId(resource.getStructureId());
319                if (row.getOriginalStructureId() == null) {
320                    row.setOriginalStructureId(resource.getStructureId());
321                }
322            } catch (CmsException e) {
323                row.setPathError(messageResourceNotFound(locale));
324                m_hasErrors = true;
325            }
326        }
327        if (!CmsAlias.ALIAS_PATTERN.matcher(row.getAliasPath()).matches()) {
328            row.setAliasError(messageInvalidAliasPath(locale));
329            m_hasErrors = true;
330        }
331    }
332}