001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.db;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsResource;
032import org.opencms.gwt.shared.alias.CmsAliasImportResult;
033import org.opencms.gwt.shared.alias.CmsAliasImportStatus;
034import org.opencms.gwt.shared.alias.CmsAliasMode;
035import org.opencms.i18n.CmsEncoder;
036import org.opencms.lock.CmsLock;
037import org.opencms.main.CmsException;
038import org.opencms.main.CmsLog;
039import org.opencms.main.OpenCms;
040import org.opencms.security.CmsRole;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.util.CmsUUID;
043
044import java.io.BufferedReader;
045import java.io.ByteArrayInputStream;
046import java.io.IOException;
047import java.io.InputStreamReader;
048import java.util.ArrayList;
049import java.util.Collection;
050import java.util.Collections;
051import java.util.Comparator;
052import java.util.HashSet;
053import java.util.List;
054import java.util.Locale;
055import java.util.Set;
056
057import org.apache.commons.logging.Log;
058
059import com.google.common.collect.ArrayListMultimap;
060import com.google.common.collect.Multimap;
061
062import au.com.bytecode.opencsv.CSVParser;
063
064/**
065 * The alias manager provides access to the aliases stored in the database.<p>
066 */
067public class CmsAliasManager {
068
069    /** The logger instance for this class. */
070    private static final Log LOG = CmsLog.getLog(CmsAliasManager.class);
071
072    /** The security manager for accessing the database. */
073    protected CmsSecurityManager m_securityManager;
074
075    /**
076     * Creates a new alias manager instance.<p>
077     *
078     * @param securityManager the security manager
079     */
080    public CmsAliasManager(CmsSecurityManager securityManager) {
081
082        m_securityManager = securityManager;
083    }
084
085    /**
086     * Gets the list of aliases for a path in a given site.<p>
087     *
088     * This should only return either an empty list or a list with a single element.
089     *
090     *
091     * @param cms the current CMS context
092     * @param siteRoot the site root for which we want the aliases
093     * @param aliasPath the alias path
094     *
095     * @return the aliases for the given site root and path
096     *
097     * @throws CmsException if something goes wrong
098     */
099    public List<CmsAlias> getAliasesForPath(CmsObject cms, String siteRoot, String aliasPath) throws CmsException {
100
101        CmsAlias alias = m_securityManager.readAliasByPath(cms.getRequestContext(), siteRoot, aliasPath);
102        if (alias == null) {
103            return Collections.emptyList();
104        } else {
105            return Collections.singletonList(alias);
106        }
107    }
108
109    /**
110     * Gets the list of aliases for a given site root.<p>
111     *
112     * @param cms the current CMS context
113     * @param siteRoot the site root
114     * @return the list of aliases for the given site
115     *
116     * @throws CmsException if something goes wrong
117     */
118    public List<CmsAlias> getAliasesForSite(CmsObject cms, String siteRoot) throws CmsException {
119
120        return m_securityManager.getAliasesForSite(cms.getRequestContext(), siteRoot);
121    }
122
123    /**
124     * Gets the aliases for a given structure id.<p>
125     *
126     * @param cms the current CMS context
127     * @param structureId the structure id of a resource
128     *
129     * @return the aliases which point to the resource with the given structure id
130     *
131     * @throws CmsException if something goes wrong
132     */
133    public List<CmsAlias> getAliasesForStructureId(CmsObject cms, CmsUUID structureId) throws CmsException {
134
135        List<CmsAlias> aliases = m_securityManager.readAliasesById(cms.getRequestContext(), structureId);
136        Collections.sort(aliases, new Comparator<CmsAlias>() {
137
138            public int compare(CmsAlias first, CmsAlias second) {
139
140                return first.getAliasPath().compareTo(second.getAliasPath());
141            }
142        });
143        return aliases;
144    }
145
146    /**
147     * Reads the rewrite aliases for a given site root.<p>
148     *
149     * @param cms the current CMS context
150     * @param siteRoot the site root for which the rewrite aliases should be retrieved
151     * @return the list of rewrite aliases for the given site root
152     *
153     * @throws CmsException if something goes wrong
154     */
155    public List<CmsRewriteAlias> getRewriteAliases(CmsObject cms, String siteRoot) throws CmsException {
156
157        CmsRewriteAliasFilter filter = new CmsRewriteAliasFilter().setSiteRoot(siteRoot);
158        List<CmsRewriteAlias> result = m_securityManager.getRewriteAliases(cms.getRequestContext(), filter);
159        return result;
160    }
161
162    /**
163     * Gets the rewrite alias matcher for the given site.<p>
164     *
165     * @param cms the CMS context to use
166     * @param siteRoot the site root
167     *
168     * @return the alias matcher for the site with the given site root
169     *
170     * @throws CmsException if something goes wrong
171     */
172    public CmsRewriteAliasMatcher getRewriteAliasMatcher(CmsObject cms, String siteRoot) throws CmsException {
173
174        List<CmsRewriteAlias> aliases = getRewriteAliases(cms, siteRoot);
175        return new CmsRewriteAliasMatcher(aliases);
176    }
177
178    /**
179     * Checks whether the current user has permissions for mass editing the alias table.<p>
180     *
181     * @param cms the current CMS context
182     * @param siteRoot the site root to check
183     * @return true if the user from the CMS context is allowed to mass edit the alias table
184     */
185    public boolean hasPermissionsForMassEdit(CmsObject cms, String siteRoot) {
186
187        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
188        try {
189            cms.getRequestContext().setSiteRoot(siteRoot);
190            return OpenCms.getRoleManager().hasRoleForResource(cms, CmsRole.ADMINISTRATOR, "/");
191        } finally {
192            cms.getRequestContext().setSiteRoot(originalSiteRoot);
193        }
194
195    }
196
197    /**
198     * Imports alias CSV data.<p>
199     *
200     * @param cms the current CMS context
201     * @param aliasData the alias data
202     * @param siteRoot the root of the site into which the alias data should be imported
203     * @param separator the field separator which is used by the imported data
204     * @return the list of import results
205     *
206     * @throws Exception if something goes wrong
207     */
208    public synchronized List<CmsAliasImportResult> importAliases(
209        CmsObject cms,
210        byte[] aliasData,
211        String siteRoot,
212        String separator) throws Exception {
213
214        checkPermissionsForMassEdit(cms);
215        BufferedReader reader = new BufferedReader(
216            new InputStreamReader(new ByteArrayInputStream(aliasData), CmsEncoder.ENCODING_UTF_8));
217        String line = reader.readLine();
218        List<CmsAliasImportResult> totalResult = new ArrayList<CmsAliasImportResult>();
219        CmsAliasImportResult result;
220        while (line != null) {
221            result = processAliasLine(cms, siteRoot, line, separator);
222            if (result != null) {
223                totalResult.add(result);
224            }
225            line = reader.readLine();
226        }
227        return totalResult;
228    }
229
230    /**
231     * Saves the aliases for a given structure id, <b>completely replacing</b> any existing aliases for the same structure id.<p>
232     *
233     * @param cms the current CMS context
234     * @param structureId the structure id of a resource
235     * @param aliases the list of aliases which should be written
236     *
237     * @throws CmsException if something goes wrong
238     */
239    public synchronized void saveAliases(CmsObject cms, CmsUUID structureId, List<CmsAlias> aliases)
240    throws CmsException {
241
242        m_securityManager.saveAliases(cms.getRequestContext(), cms.readResource(structureId), aliases);
243        touch(cms, cms.readResource(structureId));
244    }
245
246    /**
247     * Saves the rewrite alias for a given site root.<p>
248     *
249     * @param cms the current CMS context
250     * @param siteRoot the site root for which the rewrite aliases should be saved
251     * @param newAliases the list of aliases to save
252     *
253     * @throws CmsException if something goes wrong
254     */
255    public void saveRewriteAliases(CmsObject cms, String siteRoot, List<CmsRewriteAlias> newAliases)
256    throws CmsException {
257
258        checkPermissionsForMassEdit(cms, siteRoot);
259        m_securityManager.saveRewriteAliases(cms.getRequestContext(), siteRoot, newAliases);
260    }
261
262    /**
263     * Updates the aliases in the database.<p>
264     *
265     * @param cms the current CMS context
266     * @param toDelete the collection of aliases to delete
267     * @param toAdd the collection of aliases to add
268     * @throws CmsException if something goes wrong
269     */
270    public synchronized void updateAliases(CmsObject cms, Collection<CmsAlias> toDelete, Collection<CmsAlias> toAdd)
271    throws CmsException {
272
273        checkPermissionsForMassEdit(cms);
274        Set<CmsUUID> allKeys = new HashSet<CmsUUID>();
275        Multimap<CmsUUID, CmsAlias> toDeleteMap = ArrayListMultimap.create();
276
277        // first, group the aliases by structure id
278
279        for (CmsAlias alias : toDelete) {
280            toDeleteMap.put(alias.getStructureId(), alias);
281            allKeys.add(alias.getStructureId());
282        }
283
284        Multimap<CmsUUID, CmsAlias> toAddMap = ArrayListMultimap.create();
285        for (CmsAlias alias : toAdd) {
286            toAddMap.put(alias.getStructureId(), alias);
287            allKeys.add(alias.getStructureId());
288        }
289
290        // Do all the deletions first, so we don't run into duplicate key errors for the alias paths
291        for (CmsUUID structureId : allKeys) {
292            Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId));
293            Collection<CmsAlias> toDeleteForId = toDeleteMap.get(structureId);
294            if ((toDeleteForId != null) && !toDeleteForId.isEmpty()) {
295                aliasesToSave.removeAll(toDeleteForId);
296            }
297            saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave));
298        }
299        for (CmsUUID structureId : allKeys) {
300            Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId));
301            Collection<CmsAlias> toAddForId = toAddMap.get(structureId);
302            if ((toAddForId != null) && !toAddForId.isEmpty()) {
303                aliasesToSave.addAll(toAddForId);
304            }
305            saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave));
306        }
307    }
308
309    /**
310     * Checks whether the current user has the permissions to mass edit the alias table, and throws an
311     * exception otherwise.<p>
312     *
313     * @param cms the current CMS context
314     *
315     * @throws CmsException
316     */
317    protected void checkPermissionsForMassEdit(CmsObject cms) throws CmsException {
318
319        OpenCms.getRoleManager().checkRoleForResource(cms, CmsRole.ADMINISTRATOR, "/");
320    }
321
322    /**
323     * Imports a single alias.<p>
324     *
325     * @param cms the current CMS context
326     * @param siteRoot the site root
327     * @param aliasPath the alias path
328     * @param vfsPath the VFS path
329     * @param mode the alias mode
330     *
331     * @return the result of the import
332     *
333     * @throws CmsException if something goes wrong
334     */
335    protected synchronized CmsAliasImportResult importAlias(
336        CmsObject cms,
337        String siteRoot,
338        String aliasPath,
339        String vfsPath,
340        CmsAliasMode mode) throws CmsException {
341
342        CmsResource resource;
343        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
344        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
345        try {
346            cms.getRequestContext().setSiteRoot(siteRoot);
347            resource = cms.readResource(vfsPath);
348        } catch (CmsException e) {
349            return new CmsAliasImportResult(
350                CmsAliasImportStatus.aliasImportError,
351                messageImportCantReadResource(locale, vfsPath),
352                aliasPath,
353                vfsPath,
354                mode);
355        } finally {
356            cms.getRequestContext().setSiteRoot(originalSiteRoot);
357        }
358        if (!CmsAlias.ALIAS_PATTERN.matcher(aliasPath).matches()) {
359            return new CmsAliasImportResult(
360                CmsAliasImportStatus.aliasImportError,
361                messageImportInvalidAliasPath(locale, aliasPath),
362                aliasPath,
363                vfsPath,
364                mode);
365        }
366        List<CmsAlias> maybeAlias = getAliasesForPath(cms, siteRoot, aliasPath);
367        if (maybeAlias.isEmpty()) {
368            CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode);
369            m_securityManager.addAlias(cms.getRequestContext(), newAlias);
370            touch(cms, resource);
371            return new CmsAliasImportResult(
372                CmsAliasImportStatus.aliasNew,
373                messageImportOk(locale),
374                aliasPath,
375                vfsPath,
376                mode);
377        } else {
378            CmsAlias existingAlias = maybeAlias.get(0);
379            CmsAliasFilter deleteFilter = new CmsAliasFilter(
380                siteRoot,
381                existingAlias.getAliasPath(),
382                existingAlias.getStructureId());
383            m_securityManager.deleteAliases(cms.getRequestContext(), deleteFilter);
384            CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode);
385            m_securityManager.addAlias(cms.getRequestContext(), newAlias);
386            touch(cms, resource);
387            return new CmsAliasImportResult(
388                CmsAliasImportStatus.aliasChanged,
389                messageImportUpdate(locale),
390                aliasPath,
391                vfsPath,
392                mode);
393        }
394    }
395
396    /**
397     * Processes a single alias import operation which has already been parsed into fields.<p>
398     *
399     * @param cms the current CMS context
400     * @param siteRoot the site root
401     * @param aliasPath the alias path
402     * @param vfsPath the VFS resource path
403     * @param mode the alias mode
404     *
405     * @return the result of the import operation
406     */
407    protected CmsAliasImportResult processAliasImport(
408        CmsObject cms,
409        String siteRoot,
410        String aliasPath,
411        String vfsPath,
412        CmsAliasMode mode) {
413
414        try {
415            return importAlias(cms, siteRoot, aliasPath, vfsPath, mode);
416        } catch (CmsException e) {
417            return new CmsAliasImportResult(
418                CmsAliasImportStatus.aliasImportError,
419                e.getLocalizedMessage(),
420                aliasPath,
421                vfsPath,
422                mode);
423        }
424    }
425
426    /**
427     * Processes a line from a CSV file containing the alias data to be imported.<p>
428     *
429     * @param cms the current CMS context
430     * @param siteRoot the site root
431     * @param line the line with the data to import
432     * @param separator the field separator
433     *
434     * @return the import result
435     */
436    protected CmsAliasImportResult processAliasLine(CmsObject cms, String siteRoot, String line, String separator) {
437
438        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
439        line = line.trim();
440        // ignore empty lines or comments starting with #
441        if (CmsStringUtil.isEmptyOrWhitespaceOnly(line) || line.startsWith("#")) {
442            return null;
443        }
444        CSVParser parser = new CSVParser(separator.charAt(0));
445        String[] tokens = null;
446        try {
447            tokens = parser.parseLine(line);
448            for (int i = 0; i < tokens.length; i++) {
449                tokens[i] = tokens[i].trim();
450            }
451        } catch (IOException e) {
452            return new CmsAliasImportResult(
453                line,
454                CmsAliasImportStatus.aliasParseError,
455                messageImportInvalidFormat(locale));
456        }
457        int numTokens = tokens.length;
458        String alias = null;
459        String vfsPath = null;
460        if (numTokens >= 2) {
461            alias = tokens[0];
462            vfsPath = tokens[1];
463        }
464        CmsAliasMode mode = CmsAliasMode.permanentRedirect;
465        if (numTokens >= 3) {
466            try {
467                mode = CmsAliasMode.valueOf(tokens[2].trim());
468            } catch (Exception e) {
469                return new CmsAliasImportResult(
470                    line,
471                    CmsAliasImportStatus.aliasParseError,
472                    messageImportInvalidFormat(locale));
473            }
474        }
475        boolean isRewrite = false;
476        if (numTokens == 4) {
477            if (!tokens[3].equals("rewrite")) {
478                return new CmsAliasImportResult(
479                    line,
480                    CmsAliasImportStatus.aliasParseError,
481                    messageImportInvalidFormat(locale));
482            } else {
483                isRewrite = true;
484            }
485        }
486        if ((numTokens < 2) || (numTokens > 4)) {
487            return new CmsAliasImportResult(
488                line,
489                CmsAliasImportStatus.aliasParseError,
490                messageImportInvalidFormat(locale));
491        }
492        CmsAliasImportResult returnValue = null;
493        if (isRewrite) {
494            returnValue = processRewriteImport(cms, siteRoot, alias, vfsPath, mode);
495        } else {
496            returnValue = processAliasImport(cms, siteRoot, alias, vfsPath, mode);
497        }
498        returnValue.setLine(line);
499        return returnValue;
500    }
501
502    /**
503     * Checks that the user has permissions for a mass edit operation in a given site.<p>
504     *
505     * @param cms the current CMS context
506     * @param siteRoot the site for which the permissions should be checked
507     *
508     * @throws CmsException if something goes wrong
509     */
510    private void checkPermissionsForMassEdit(CmsObject cms, String siteRoot) throws CmsException {
511
512        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
513        try {
514            cms.getRequestContext().setSiteRoot(siteRoot);
515            checkPermissionsForMassEdit(cms);
516        } finally {
517            cms.getRequestContext().setSiteRoot(originalSiteRoot);
518        }
519    }
520
521    /**
522     * Message accessor.<p>
523     *
524     * @param locale the message locale
525     * @param path a path
526     *
527     * @return the message string
528     */
529    private String messageImportCantReadResource(Locale locale, String path) {
530
531        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_COULD_NOT_READ_RESOURCE_0);
532
533    }
534
535    /**
536     * Message accessor.<p>
537     *
538     * @param locale the message locale
539     * @param path a path
540     *
541     * @return the message string
542     */
543    private String messageImportInvalidAliasPath(Locale locale, String path) {
544
545        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_INVALID_ALIAS_PATH_0);
546
547    }
548
549    /**
550     * Message accessor.<p>
551     *
552     * @param locale the message locale
553     *
554     * @return the message string
555     */
556    private String messageImportInvalidFormat(Locale locale) {
557
558        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_BAD_FORMAT_0);
559    }
560
561    /**
562     * Message accessor.<p>
563     *
564     * @param locale the message locale
565     *
566     * @return the message string
567     */
568    private String messageImportOk(Locale locale) {
569
570        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_OK_0);
571    }
572
573    /**
574     * Message accessor.<p>
575     *
576     * @param locale the message locale
577     *
578     * @return the message string
579     */
580    private String messageImportUpdate(Locale locale) {
581
582        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_UPDATED_0);
583    }
584
585    /**
586     * Handles the import of a rewrite alias.<p>
587     *
588     * @param cms the current CMS context
589     * @param siteRoot the site root
590     * @param source the rewrite pattern
591     * @param target the rewrite replacement
592     * @param mode the alias mode
593     *
594     * @return the import result
595     */
596    private CmsAliasImportResult processRewriteImport(
597        CmsObject cms,
598        String siteRoot,
599        String source,
600        String target,
601        CmsAliasMode mode) {
602
603        try {
604            return m_securityManager.importRewriteAlias(cms.getRequestContext(), siteRoot, source, target, mode);
605        } catch (CmsException e) {
606            return new CmsAliasImportResult(
607                CmsAliasImportStatus.aliasImportError,
608                e.getLocalizedMessage(),
609                source,
610                target,
611                mode);
612        }
613
614    }
615
616    /**
617     * Tries to to touch a resource by setting its last modification date, but only if its state is 'unchanged'.<p>
618     *
619     * @param cms the current CMS context
620     * @param resource the resource which should be 'touched'.
621     */
622    private void touch(CmsObject cms, CmsResource resource) {
623
624        if (resource.getState().isUnchanged()) {
625            try {
626                CmsLock lock = cms.getLock(resource);
627                if (lock.isUnlocked() || !lock.isOwnedBy(cms.getRequestContext().getCurrentUser())) {
628                    cms.lockResourceTemporary(resource);
629                    long now = System.currentTimeMillis();
630                    resource.setDateLastModified(now);
631                    cms.writeResource(resource);
632                    if (lock.isUnlocked()) {
633                        cms.unlockResource(resource);
634                    }
635                }
636            } catch (CmsException e) {
637                LOG.warn("Could not touch resource after alias modification: " + resource.getRootPath(), e);
638            }
639        }
640    }
641
642}