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)
213    throws Exception {
214
215        checkPermissionsForMassEdit(cms);
216        BufferedReader reader = new BufferedReader(
217            new InputStreamReader(new ByteArrayInputStream(aliasData), CmsEncoder.ENCODING_UTF_8));
218        String line = reader.readLine();
219        List<CmsAliasImportResult> totalResult = new ArrayList<CmsAliasImportResult>();
220        CmsAliasImportResult result;
221        while (line != null) {
222            result = processAliasLine(cms, siteRoot, line, separator);
223            if (result != null) {
224                totalResult.add(result);
225            }
226            line = reader.readLine();
227        }
228        return totalResult;
229    }
230
231    /**
232     * Saves the aliases for a given structure id, <b>completely replacing</b> any existing aliases for the same structure id.<p>
233     *
234     * @param cms the current CMS context
235     * @param structureId the structure id of a resource
236     * @param aliases the list of aliases which should be written
237     *
238     * @throws CmsException if something goes wrong
239     */
240    public synchronized void saveAliases(CmsObject cms, CmsUUID structureId, List<CmsAlias> aliases)
241    throws CmsException {
242
243        m_securityManager.saveAliases(cms.getRequestContext(), cms.readResource(structureId), aliases);
244        touch(cms, cms.readResource(structureId));
245    }
246
247    /**
248     * Saves the rewrite alias for a given site root.<p>
249     *
250     * @param cms the current CMS context
251     * @param siteRoot the site root for which the rewrite aliases should be saved
252     * @param newAliases the list of aliases to save
253     *
254     * @throws CmsException if something goes wrong
255     */
256    public void saveRewriteAliases(CmsObject cms, String siteRoot, List<CmsRewriteAlias> newAliases)
257    throws CmsException {
258
259        checkPermissionsForMassEdit(cms, siteRoot);
260        m_securityManager.saveRewriteAliases(cms.getRequestContext(), siteRoot, newAliases);
261    }
262
263    /**
264     * Updates the aliases in the database.<p>
265     *
266     * @param cms the current CMS context
267     * @param toDelete the collection of aliases to delete
268     * @param toAdd the collection of aliases to add
269     * @throws CmsException if something goes wrong
270     */
271    public synchronized void updateAliases(CmsObject cms, Collection<CmsAlias> toDelete, Collection<CmsAlias> toAdd)
272    throws CmsException {
273
274        checkPermissionsForMassEdit(cms);
275        Set<CmsUUID> allKeys = new HashSet<CmsUUID>();
276        Multimap<CmsUUID, CmsAlias> toDeleteMap = ArrayListMultimap.create();
277
278        // first, group the aliases by structure id
279
280        for (CmsAlias alias : toDelete) {
281            toDeleteMap.put(alias.getStructureId(), alias);
282            allKeys.add(alias.getStructureId());
283        }
284
285        Multimap<CmsUUID, CmsAlias> toAddMap = ArrayListMultimap.create();
286        for (CmsAlias alias : toAdd) {
287            toAddMap.put(alias.getStructureId(), alias);
288            allKeys.add(alias.getStructureId());
289        }
290
291        // Do all the deletions first, so we don't run into duplicate key errors for the alias paths
292        for (CmsUUID structureId : allKeys) {
293            Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId));
294            Collection<CmsAlias> toDeleteForId = toDeleteMap.get(structureId);
295            if ((toDeleteForId != null) && !toDeleteForId.isEmpty()) {
296                aliasesToSave.removeAll(toDeleteForId);
297            }
298            saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave));
299        }
300        for (CmsUUID structureId : allKeys) {
301            Set<CmsAlias> aliasesToSave = new HashSet<CmsAlias>(getAliasesForStructureId(cms, structureId));
302            Collection<CmsAlias> toAddForId = toAddMap.get(structureId);
303            if ((toAddForId != null) && !toAddForId.isEmpty()) {
304                aliasesToSave.addAll(toAddForId);
305            }
306            saveAliases(cms, structureId, new ArrayList<CmsAlias>(aliasesToSave));
307        }
308    }
309
310    /**
311     * Checks whether the current user has the permissions to mass edit the alias table, and throws an
312     * exception otherwise.<p>
313     *
314     * @param cms the current CMS context
315     *
316     * @throws CmsException
317     */
318    protected void checkPermissionsForMassEdit(CmsObject cms) throws CmsException {
319
320        OpenCms.getRoleManager().checkRoleForResource(cms, CmsRole.ADMINISTRATOR, "/");
321    }
322
323    /**
324     * Imports a single alias.<p>
325     *
326     * @param cms the current CMS context
327     * @param siteRoot the site root
328     * @param aliasPath the alias path
329     * @param vfsPath the VFS path
330     * @param mode the alias mode
331     *
332     * @return the result of the import
333     *
334     * @throws CmsException if something goes wrong
335     */
336    protected synchronized CmsAliasImportResult importAlias(
337        CmsObject cms,
338        String siteRoot,
339        String aliasPath,
340        String vfsPath,
341        CmsAliasMode mode)
342    throws CmsException {
343
344        CmsResource resource;
345        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
346        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
347        try {
348            cms.getRequestContext().setSiteRoot(siteRoot);
349            resource = cms.readResource(vfsPath);
350        } catch (CmsException e) {
351            return new CmsAliasImportResult(
352                CmsAliasImportStatus.aliasImportError,
353                messageImportCantReadResource(locale, vfsPath),
354                aliasPath,
355                vfsPath,
356                mode);
357        } finally {
358            cms.getRequestContext().setSiteRoot(originalSiteRoot);
359        }
360        if (!CmsAlias.ALIAS_PATTERN.matcher(aliasPath).matches()) {
361            return new CmsAliasImportResult(
362                CmsAliasImportStatus.aliasImportError,
363                messageImportInvalidAliasPath(locale, aliasPath),
364                aliasPath,
365                vfsPath,
366                mode);
367        }
368        List<CmsAlias> maybeAlias = getAliasesForPath(cms, siteRoot, aliasPath);
369        if (maybeAlias.isEmpty()) {
370            CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode);
371            m_securityManager.addAlias(cms.getRequestContext(), newAlias);
372            touch(cms, resource);
373            return new CmsAliasImportResult(
374                CmsAliasImportStatus.aliasNew,
375                messageImportOk(locale),
376                aliasPath,
377                vfsPath,
378                mode);
379        } else {
380            CmsAlias existingAlias = maybeAlias.get(0);
381            CmsAliasFilter deleteFilter = new CmsAliasFilter(
382                siteRoot,
383                existingAlias.getAliasPath(),
384                existingAlias.getStructureId());
385            m_securityManager.deleteAliases(cms.getRequestContext(), deleteFilter);
386            CmsAlias newAlias = new CmsAlias(resource.getStructureId(), siteRoot, aliasPath, mode);
387            m_securityManager.addAlias(cms.getRequestContext(), newAlias);
388            touch(cms, resource);
389            return new CmsAliasImportResult(
390                CmsAliasImportStatus.aliasChanged,
391                messageImportUpdate(locale),
392                aliasPath,
393                vfsPath,
394                mode);
395        }
396    }
397
398    /**
399     * Processes a single alias import operation which has already been parsed into fields.<p>
400     *
401     * @param cms the current CMS context
402     * @param siteRoot the site root
403     * @param aliasPath the alias path
404     * @param vfsPath the VFS resource path
405     * @param mode the alias mode
406     *
407     * @return the result of the import operation
408     */
409    protected CmsAliasImportResult processAliasImport(
410        CmsObject cms,
411        String siteRoot,
412        String aliasPath,
413        String vfsPath,
414        CmsAliasMode mode) {
415
416        try {
417            return importAlias(cms, siteRoot, aliasPath, vfsPath, mode);
418        } catch (CmsException e) {
419            return new CmsAliasImportResult(
420                CmsAliasImportStatus.aliasImportError,
421                e.getLocalizedMessage(),
422                aliasPath,
423                vfsPath,
424                mode);
425        }
426    }
427
428    /**
429     * Processes a line from a CSV file containing the alias data to be imported.<p>
430     *
431     * @param cms the current CMS context
432     * @param siteRoot the site root
433     * @param line the line with the data to import
434     * @param separator the field separator
435     *
436     * @return the import result
437     */
438    protected CmsAliasImportResult processAliasLine(CmsObject cms, String siteRoot, String line, String separator) {
439
440        Locale locale = OpenCms.getWorkplaceManager().getWorkplaceLocale(cms);
441        line = line.trim();
442        // ignore empty lines or comments starting with #
443        if (CmsStringUtil.isEmptyOrWhitespaceOnly(line) || line.startsWith("#")) {
444            return null;
445        }
446        CSVParser parser = new CSVParser(separator.charAt(0));
447        String[] tokens = null;
448        try {
449            tokens = parser.parseLine(line);
450            for (int i = 0; i < tokens.length; i++) {
451                tokens[i] = tokens[i].trim();
452            }
453        } catch (IOException e) {
454            return new CmsAliasImportResult(
455                line,
456                CmsAliasImportStatus.aliasParseError,
457                messageImportInvalidFormat(locale));
458        }
459        int numTokens = tokens.length;
460        String alias = null;
461        String vfsPath = null;
462        if (numTokens >= 2) {
463            alias = tokens[0];
464            vfsPath = tokens[1];
465        }
466        CmsAliasMode mode = CmsAliasMode.permanentRedirect;
467        if (numTokens >= 3) {
468            try {
469                mode = CmsAliasMode.valueOf(tokens[2].trim());
470            } catch (Exception e) {
471                return new CmsAliasImportResult(
472                    line,
473                    CmsAliasImportStatus.aliasParseError,
474                    messageImportInvalidFormat(locale));
475            }
476        }
477        boolean isRewrite = false;
478        if (numTokens == 4) {
479            if (!tokens[3].equals("rewrite")) {
480                return new CmsAliasImportResult(
481                    line,
482                    CmsAliasImportStatus.aliasParseError,
483                    messageImportInvalidFormat(locale));
484            } else {
485                isRewrite = true;
486            }
487        }
488        if ((numTokens < 2) || (numTokens > 4)) {
489            return new CmsAliasImportResult(
490                line,
491                CmsAliasImportStatus.aliasParseError,
492                messageImportInvalidFormat(locale));
493        }
494        CmsAliasImportResult returnValue = null;
495        if (isRewrite) {
496            returnValue = processRewriteImport(cms, siteRoot, alias, vfsPath, mode);
497        } else {
498            returnValue = processAliasImport(cms, siteRoot, alias, vfsPath, mode);
499        }
500        returnValue.setLine(line);
501        return returnValue;
502    }
503
504    /**
505     * Checks that the user has permissions for a mass edit operation in a given site.<p>
506     *
507     * @param cms the current CMS context
508     * @param siteRoot the site for which the permissions should be checked
509     *
510     * @throws CmsException if something goes wrong
511     */
512    private void checkPermissionsForMassEdit(CmsObject cms, String siteRoot) throws CmsException {
513
514        String originalSiteRoot = cms.getRequestContext().getSiteRoot();
515        try {
516            cms.getRequestContext().setSiteRoot(siteRoot);
517            checkPermissionsForMassEdit(cms);
518        } finally {
519            cms.getRequestContext().setSiteRoot(originalSiteRoot);
520        }
521    }
522
523    /**
524     * Message accessor.<p>
525     *
526     * @param locale the message locale
527     * @param path a path
528     *
529     * @return the message string
530     */
531    private String messageImportCantReadResource(Locale locale, String path) {
532
533        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_COULD_NOT_READ_RESOURCE_0);
534
535    }
536
537    /**
538     * Message accessor.<p>
539     *
540     * @param locale the message locale
541     * @param path a path
542     *
543     * @return the message string
544     */
545    private String messageImportInvalidAliasPath(Locale locale, String path) {
546
547        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_INVALID_ALIAS_PATH_0);
548
549    }
550
551    /**
552     * Message accessor.<p>
553     *
554     * @param locale the message locale
555     *
556     * @return the message string
557     */
558    private String messageImportInvalidFormat(Locale locale) {
559
560        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_BAD_FORMAT_0);
561    }
562
563    /**
564     * Message accessor.<p>
565     *
566     * @param locale the message locale
567     *
568     * @return the message string
569     */
570    private String messageImportOk(Locale locale) {
571
572        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_OK_0);
573    }
574
575    /**
576     * Message accessor.<p>
577     *
578     * @param locale the message locale
579     *
580     * @return the message string
581     */
582    private String messageImportUpdate(Locale locale) {
583
584        return Messages.get().getBundle(locale).key(Messages.ERR_ALIAS_IMPORT_UPDATED_0);
585    }
586
587    /**
588     * Handles the import of a rewrite alias.<p>
589     *
590     * @param cms the current CMS context
591     * @param siteRoot the site root
592     * @param source the rewrite pattern
593     * @param target the rewrite replacement
594     * @param mode the alias mode
595     *
596     * @return the import result
597     */
598    private CmsAliasImportResult processRewriteImport(
599        CmsObject cms,
600        String siteRoot,
601        String source,
602        String target,
603        CmsAliasMode mode) {
604
605        try {
606            return m_securityManager.importRewriteAlias(cms.getRequestContext(), siteRoot, source, target, mode);
607        } catch (CmsException e) {
608            return new CmsAliasImportResult(
609                CmsAliasImportStatus.aliasImportError,
610                e.getLocalizedMessage(),
611                source,
612                target,
613                mode);
614        }
615
616    }
617
618    /**
619     * Tries to to touch a resource by setting its last modification date, but only if its state is 'unchanged'.<p>
620     *
621     * @param cms the current CMS context
622     * @param resource the resource which should be 'touched'.
623     */
624    private void touch(CmsObject cms, CmsResource resource) {
625
626        if (resource.getState().isUnchanged()) {
627            try {
628                CmsLock lock = cms.getLock(resource);
629                if (lock.isUnlocked() || !lock.isOwnedBy(cms.getRequestContext().getCurrentUser())) {
630                    cms.lockResourceTemporary(resource);
631                    long now = System.currentTimeMillis();
632                    resource.setDateLastModified(now);
633                    cms.writeResource(resource);
634                    if (lock.isUnlocked()) {
635                        cms.unlockResource(resource);
636                    }
637                }
638            } catch (CmsException e) {
639                LOG.warn("Could not touch resource after alias modification: " + resource.getRootPath(), e);
640            }
641        }
642    }
643
644}