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.file;
029
030import org.opencms.file.types.A_CmsResourceTypeLinkParseable;
031import org.opencms.file.types.CmsResourceTypeJsp;
032import org.opencms.file.types.I_CmsResourceType;
033import org.opencms.i18n.CmsEncoder;
034import org.opencms.loader.CmsLoaderException;
035import org.opencms.lock.CmsLock;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsIllegalArgumentException;
038import org.opencms.main.CmsLog;
039import org.opencms.main.OpenCms;
040import org.opencms.relations.CmsRelation;
041import org.opencms.relations.CmsRelationFilter;
042import org.opencms.relations.CmsRelationType;
043import org.opencms.relations.I_CmsLinkParseable;
044import org.opencms.util.CmsFileUtil;
045import org.opencms.util.CmsPair;
046import org.opencms.util.CmsStringUtil;
047import org.opencms.util.CmsUUID;
048import org.opencms.util.I_CmsRegexSubstitution;
049import org.opencms.xml.CmsXmlEntityResolver;
050import org.opencms.xml.CmsXmlException;
051import org.opencms.xml.CmsXmlUtils;
052import org.opencms.xml.content.Messages;
053
054import java.io.UnsupportedEncodingException;
055import java.util.ArrayList;
056import java.util.Collection;
057import java.util.HashMap;
058import java.util.HashSet;
059import java.util.List;
060import java.util.Map;
061import java.util.Set;
062import java.util.regex.Matcher;
063import java.util.regex.Pattern;
064
065import org.apache.commons.logging.Log;
066
067import org.dom4j.Document;
068
069import com.google.common.collect.ArrayListMultimap;
070import com.google.common.collect.Lists;
071import com.google.common.collect.Multimap;
072
073/**
074 * A class used to rewrite links and relations in one subtree such that relations from that subtree to another given subtree
075 * replaced with relations to the first subtree.<p>
076 */
077public class CmsLinkRewriter {
078
079    /** The logger instance for this class. */
080    private static final Log LOG = CmsLog.getLog(CmsLinkRewriter.class);
081
082    /** A map from source folder structure ids to corresponding target folder resources. */
083    protected Map<CmsUUID, CmsResource> m_translationsById = new HashMap<CmsUUID, CmsResource>();
084
085    /** A map from source folder root paths to the corresponding target folder resources. */
086    protected Map<String, CmsResource> m_translationsByPath = new HashMap<String, CmsResource>();
087
088    /** A map of resources which have been cached by structure id. */
089    private Map<CmsUUID, CmsResource> m_cachedResources = new HashMap<CmsUUID, CmsResource>();
090
091    /** The CMS object used for file operations. */
092    private CmsObject m_cms;
093
094    /** If true, all XML contents will be rewritten instead of just those containing links to correct. */
095    private boolean m_rewriteAllXmlContents = true;
096
097    /** The set of structure ids of resources whose content has been rewritten. */
098    private Set<CmsUUID> m_rewrittenContent = new HashSet<CmsUUID>();
099
100    /** A list of path pairs, each containing a source and a target of a copy operation. */
101    private List<CmsPair<String, String>> m_sourceTargetPairs = new ArrayList<CmsPair<String, String>>();
102
103    /** The target folder root path. */
104    private String m_targetPath;
105
106    /**
107     * Creates a link rewriter for use after a multi-copy operation.<p>
108     *
109     * @param cms the current CMS context
110     * @param sources the list of source root paths
111     * @param target the target parent folder root path
112     */
113    public CmsLinkRewriter(CmsObject cms, List<String> sources, String target) {
114
115        m_sourceTargetPairs = new ArrayList<CmsPair<String, String>>();
116        for (String source : sources) {
117            checkNotSubPath(source, target);
118            String targetSub = CmsStringUtil.joinPaths(target, CmsResource.getName(source));
119            m_sourceTargetPairs.add(CmsPair.create(source, targetSub));
120        }
121        m_targetPath = target;
122        m_cms = cms;
123    }
124
125    /**
126     * Creates a new link rewriter for a list of sources and corresponding targets.<p>
127     *
128     * @param cms the current CMS context
129     * @param targetPath the target root path
130     * @param sourceTargetPairs the list of source-target pairs
131     */
132    public CmsLinkRewriter(CmsObject cms, String targetPath, List<CmsPair<String, String>> sourceTargetPairs) {
133
134        m_cms = cms;
135        m_targetPath = targetPath;
136        m_sourceTargetPairs = sourceTargetPairs;
137    }
138
139    /**
140     * Creates a link rewriter for use after a single copy operation.<p>
141     *
142     * @param cms the current CMS context
143     * @param source the source folder root path
144     * @param target the target folder root path
145     */
146    public CmsLinkRewriter(CmsObject cms, String source, String target) {
147
148        m_sourceTargetPairs = new ArrayList<CmsPair<String, String>>();
149        checkNotSubPath(source, target);
150
151        m_sourceTargetPairs.add(CmsPair.create(source, target));
152        m_targetPath = target;
153        m_cms = cms;
154    }
155
156    /**
157     * Checks whether a given resource is a folder and throws an exception otherwise.<p>
158     *
159     * @param resource the resource to check
160     * @throws CmsException if something goes wrong
161     */
162    protected static void checkIsFolder(CmsResource resource) throws CmsException {
163
164        if (!isFolder(resource)) {
165            throw new CmsIllegalArgumentException(
166                Messages.get().container(
167                    org.opencms.file.Messages.ERR_REWRITE_LINKS_ROOT_NOT_FOLDER_1,
168                    resource.getRootPath()));
169        }
170    }
171
172    /**
173     * Helper method to check whether a given resource is a folder.<p>
174     *
175     * @param resource the resouce to check
176     * @return true if the resource is a folder
177     *
178     * @throws CmsLoaderException if the resource type couldn't be found
179     */
180    protected static boolean isFolder(CmsResource resource) throws CmsLoaderException {
181
182        I_CmsResourceType resourceType = OpenCms.getResourceManager().getResourceType(resource.getTypeId());
183        return resourceType.isFolder();
184    }
185
186    /**
187     * Starts the link rewriting process.<p>
188     *
189     * @throws CmsException if something goes wrong
190     */
191    public void rewriteLinks() throws CmsException {
192
193        init();
194        List<CmsRelation> relationsToCorrect = findRelationsFromTargetToSource();
195        // group relations by the structure id of their source
196        Multimap<CmsUUID, CmsRelation> relationsBySourceId = ArrayListMultimap.create();
197        for (CmsRelation relation : relationsToCorrect) {
198            LOG.info(
199                "Found relation which needs to be corrected: "
200                    + relation.getSourcePath()
201                    + " -> "
202                    + relation.getTargetPath()
203                    + " ["
204                    + relation.getType().getName()
205                    + "]");
206            relationsBySourceId.put(relation.getSourceId(), relation);
207        }
208
209        // make sure we have a lock on the target folder before doing any write operations
210        CmsLock lock = m_cms.getLock(m_targetPath);
211        if (lock.isUnlocked() || !lock.isOwnedBy(m_cms.getRequestContext().getCurrentUser())) {
212            // fail if locked by another user
213            m_cms.lockResource(m_targetPath);
214        }
215
216        for (CmsUUID structureId : relationsBySourceId.keySet()) {
217
218            Collection<CmsRelation> relationsForResource = relationsBySourceId.get(structureId);
219            CmsResource resource = null;
220            try {
221                resource = getResource(structureId);
222                rewriteLinks(resource, relationsForResource);
223            } catch (CmsException e) {
224                LOG.error(e.getLocalizedMessage(), e);
225            }
226        }
227        if (!m_rewriteAllXmlContents) {
228            return;
229        }
230        for (Map.Entry<CmsUUID, CmsResource> entry : m_cachedResources.entrySet()) {
231            CmsUUID key = entry.getKey();
232            CmsResource resource = entry.getValue();
233            if (isInTargets(resource.getRootPath()) && !m_rewrittenContent.contains(key)) {
234                I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(resource.getTypeId());
235                // rewrite content for other files so
236                if (resType instanceof A_CmsResourceTypeLinkParseable) {
237                    try {
238                        CmsFile file = m_cms.readFile(resource);
239                        m_cms.writeFile(file);
240                    } catch (CmsException e) {
241                        LOG.error(e.getLocalizedMessage(), e);
242                    }
243                }
244            }
245        }
246        copyLocaleRelations();
247    }
248
249    /**
250     * Sets the 'rewriteAllContents' flag, which controls whether all XML contents will be rewritten
251     * or just those whose links need to be corrected.<p>
252     *
253     * @param rewriteAllContents if true, all contents will be rewritten
254     */
255    public void setRewriteAllContents(boolean rewriteAllContents) {
256
257        m_rewriteAllXmlContents = rewriteAllContents;
258    }
259
260    /**
261     * Checks that the target path is not a subfolder of the source path.<p>
262     *
263     * @param source the source path
264     * @param target the target path
265     */
266    protected void checkNotSubPath(String source, String target) {
267
268        source = CmsStringUtil.joinPaths("/", source, "/");
269        target = CmsStringUtil.joinPaths("/", target, "/");
270        if (target.startsWith(source)) {
271            throw new CmsIllegalArgumentException(
272                org.opencms.file.Messages.get().container(
273                    org.opencms.file.Messages.ERR_REWRITE_LINKS_ROOTS_DEPENDENT_2,
274                    source,
275                    target));
276        }
277    }
278
279    /**
280     * Separate method for copying locale relations..<p>
281     *
282     * This is necessary because the default copy mechanism does not copy locale relations.
283     *
284     * @throws CmsException if something goes wrong
285     */
286    protected void copyLocaleRelations() throws CmsException {
287
288        long start = System.currentTimeMillis();
289        List<CmsRelation> localeRelations = m_cms.readRelations(
290            CmsRelationFilter.ALL.filterType(CmsRelationType.LOCALE_VARIANT));
291        for (CmsRelation rel : localeRelations) {
292            if (isInSources(rel.getSourcePath()) && isInSources(rel.getTargetPath())) {
293                CmsResource newRelationSource = m_translationsById.get(rel.getSourceId());
294                CmsResource newRelationTarget = m_translationsById.get(rel.getTargetId());
295                if ((newRelationSource != null) && (newRelationTarget != null)) {
296                    try {
297                        m_cms.addRelationToResource(
298                            newRelationSource,
299                            newRelationTarget,
300                            CmsRelationType.LOCALE_VARIANT.getName());
301                    } catch (CmsException e) {
302                        LOG.error("Could not transfer locale relation: " + e.getLocalizedMessage(), e);
303                    }
304                } else {
305                    LOG.warn("Could not transfer locale relation because source/target not found in copy: " + rel);
306                }
307            }
308        }
309        long end = System.currentTimeMillis();
310        LOG.info("Copied locale relations, took " + (end - start) + "ms");
311    }
312
313    /**
314     * Decodes a byte array into a string with a given encoding, or the default encoding if that fails.<p>
315     *
316     * @param bytes the byte array
317     * @param encoding the encoding to use
318     *
319     * @return the decoded string
320     */
321    protected String decode(byte[] bytes, String encoding) {
322
323        try {
324            return new String(bytes, encoding);
325        } catch (UnsupportedEncodingException e) {
326            return new String(bytes);
327        }
328    }
329
330    /**
331     * Decodes a file's contents and return the content string and the encoding to use for writing the file
332     * back to the VFS.<p>
333     *
334     * @param file the file to decode
335     * @return a pair (content, encoding)
336     * @throws CmsException if something goes wrong
337     */
338    protected CmsPair<String, String> decode(CmsFile file) throws CmsException {
339
340        String content = null;
341        String encoding = getConfiguredEncoding(m_cms, file);
342        I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(file.getTypeId());
343        if (resType instanceof CmsResourceTypeJsp) {
344            content = decode(file.getContents(), encoding);
345        } else {
346            try {
347                CmsXmlEntityResolver resolver = new CmsXmlEntityResolver(m_cms);
348                // parse the XML and serialize it back to  a string with the configured encoding
349                Document doc = CmsXmlUtils.unmarshalHelper(file.getContents(), resolver);
350                content = CmsXmlUtils.marshal(doc, encoding);
351            } catch (Exception e) {
352                // invalid xml structure, just use the configured encoding
353                content = decode(file.getContents(), encoding);
354            }
355        }
356        return CmsPair.create(content, encoding);
357    }
358
359    /**
360     * Finds relations from the target root folder or its children to the source root folder or its children.<p>
361     *
362     * @return the list of relations from the target to the source
363     *
364     * @throws CmsException if something goes wrong
365     */
366    protected List<CmsRelation> findRelationsFromTargetToSource() throws CmsException {
367
368        List<CmsRelation> relations = m_cms.readRelations(
369            CmsRelationFilter.SOURCES.filterPath(m_targetPath).filterIncludeChildren());
370        List<CmsRelation> result = new ArrayList<CmsRelation>();
371        for (CmsRelation rel : relations) {
372            if (isInTargets(rel.getSourcePath()) && isInSources(rel.getTargetPath())) {
373                result.add(rel);
374            }
375        }
376        return result;
377    }
378
379    /**
380     * Gets the encoding which is configured at the location of a given resource.<p>
381     *
382     * @param cms the current CMS context
383     * @param resource the resource for which the configured encoding should be retrieved
384     * @return the configured encoding for the resource
385     *
386     * @throws CmsException if something goes wrong
387     */
388    protected String getConfiguredEncoding(CmsObject cms, CmsResource resource) throws CmsException {
389
390        String encoding = null;
391        try {
392            encoding = cms.readPropertyObject(
393                resource.getRootPath(),
394                CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING,
395                true).getValue();
396        } catch (CmsException e) {
397            // encoding will be null
398        }
399        if (encoding == null) {
400            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
401        } else {
402            encoding = CmsEncoder.lookupEncoding(encoding, null);
403            if (encoding == null) {
404                throw new CmsXmlException(
405                    Messages.get().container(Messages.ERR_XMLCONTENT_INVALID_ENC_1, resource.getRootPath()));
406            }
407        }
408        return encoding;
409    }
410
411    /**
412     * Gets a list of resource pairs whose paths relative to the source/target roots passed match.<p>
413     *
414     * @param source the source root
415     * @param target the target root
416     *
417     * @return the list of matching resources
418     *
419     * @throws CmsException if something goes wrong
420     */
421    protected List<CmsPair<CmsResource, CmsResource>> getMatchingResources(String source, String target)
422    throws CmsException {
423
424        List<CmsResource> sourceResources = readTree(source);
425        Map<String, CmsResource> sourceRelative = getResourcesByRelativePath(sourceResources, source);
426
427        List<CmsResource> targetResources = readTree(target);
428        Map<String, CmsResource> targetRelative = getResourcesByRelativePath(targetResources, target);
429
430        List<CmsPair<CmsResource, CmsResource>> result = new ArrayList<CmsPair<CmsResource, CmsResource>>();
431        sourceRelative.keySet().retainAll(targetRelative.keySet());
432        for (Map.Entry<String, CmsResource> entry : sourceRelative.entrySet()) {
433            String key = entry.getKey();
434            CmsResource sourceRes = entry.getValue();
435            CmsResource targetRes = targetRelative.get(key);
436            result.add(CmsPair.create(sourceRes, targetRes));
437        }
438        return result;
439    }
440
441    /**
442     * Computes the relative path given an ancestor folder path.<p>
443     *
444     * @param ancestor the ancestor folder
445     * @param rootPath the path for which the relative path should be computed
446     *
447     * @return the relative path
448     */
449    protected String getRelativePath(String ancestor, String rootPath) {
450
451        String result = rootPath.substring(ancestor.length());
452        result = CmsStringUtil.joinPaths("/", result, "/");
453        return result;
454    }
455
456    /**
457     * Accesses a resource by structure id.<p>
458     *
459     * @param structureId the structure id of the resource
460     * @return the resource with the given structure id
461     *
462     * @throws CmsException if the resource couldn't be read
463     */
464    protected CmsResource getResource(CmsUUID structureId) throws CmsException {
465
466        if (m_cachedResources.containsKey(structureId)) {
467            return m_cachedResources.get(structureId);
468        }
469        return m_cms.readResource(structureId);
470    }
471
472    /**
473     * Collects a list of resources in a map where the key for each resource is the path relative to a given folder.<p>
474     *
475     * @param resources the resources to put in the map
476     * @param basePath the path relative to which the keys of the resulting map should be computed
477     *
478     * @return a map from relative paths to resources
479     */
480    protected Map<String, CmsResource> getResourcesByRelativePath(List<CmsResource> resources, String basePath) {
481
482        Map<String, CmsResource> result = new HashMap<String, CmsResource>();
483        for (CmsResource resource : resources) {
484            String relativeSubPath = CmsStringUtil.getRelativeSubPath(basePath, resource.getRootPath());
485            if (relativeSubPath != null) {
486                result.put(relativeSubPath, resource);
487            }
488        }
489        return result;
490    }
491
492    /**
493     * Reads the data needed for rewriting the relations from the VFS.<p>
494     *
495     * @throws CmsException if something goes wrong
496     */
497    protected void init() throws CmsException {
498
499        m_cms = OpenCms.initCmsObject(m_cms);
500        // we want to use autocorrection when writing XML contents back
501        //m_cms.getRequestContext().setAttribute(CmsXmlContent.AUTO_CORRECTION_ATTRIBUTE, Boolean.TRUE);
502        m_cms.getRequestContext().setSiteRoot("");
503        List<CmsPair<CmsResource, CmsResource>> allMatchingResources = Lists.newArrayList();
504        for (CmsPair<String, String> pair : m_sourceTargetPairs) {
505            List<CmsPair<CmsResource, CmsResource>> matchingResources = getMatchingResources(
506                pair.getFirst(),
507                pair.getSecond());
508            allMatchingResources.addAll(matchingResources);
509        }
510        for (CmsPair<CmsResource, CmsResource> resPair : allMatchingResources) {
511            CmsResource source = resPair.getFirst();
512            CmsResource target = resPair.getSecond();
513            m_translationsById.put(source.getStructureId(), target);
514            m_translationsByPath.put(source.getRootPath(), target);
515        }
516    }
517
518    /**
519     * Checks if a path belongs to one of the sources.<p>
520     *
521     * @param path a root path
522     *
523     * @return true if the path belongs to the sources
524     */
525    protected boolean isInSources(String path) {
526
527        for (CmsPair<String, String> sourceTargetPair : m_sourceTargetPairs) {
528            String source = sourceTargetPair.getFirst();
529            if (CmsStringUtil.joinPaths(path, "/").startsWith(CmsStringUtil.joinPaths(source, "/"))) {
530                return true;
531            }
532        }
533        return false;
534    }
535
536    /**
537     * Checks if a path belongs to one of the targets.<p>
538     *
539     * @param path a root path
540     *
541     * @return true if the path belongs to the targets
542     */
543    protected boolean isInTargets(String path) {
544
545        for (CmsPair<String, String> sourceTargetPair : m_sourceTargetPairs) {
546            String target = sourceTargetPair.getSecond();
547            if (CmsStringUtil.joinPaths(path, "/").startsWith(CmsStringUtil.joinPaths(target, "/"))) {
548                return true;
549            }
550        }
551        return false;
552    }
553
554    /**
555     * Reads the resources in a subtree.<p>
556     *
557     * @param rootPath the root of the subtree
558     *
559     * @return the list of resources from the subtree
560     *
561     * @throws CmsException if something goes wrong
562     */
563    protected List<CmsResource> readTree(String rootPath) throws CmsException {
564
565        rootPath = CmsFileUtil.removeTrailingSeparator(rootPath);
566        CmsResource base = m_cms.readResource(rootPath);
567
568        I_CmsResourceType resType = OpenCms.getResourceManager().getResourceType(base);
569        List<CmsResource> result = new ArrayList<CmsResource>();
570        if (resType.isFolder()) {
571            rootPath = CmsStringUtil.joinPaths(rootPath, "/");
572            List<CmsResource> subResources = m_cms.readResources(rootPath, CmsResourceFilter.ALL, true);
573            result.add(base);
574            result.addAll(subResources);
575        } else {
576            result.add(base);
577        }
578        for (CmsResource resource : result) {
579            m_cachedResources.put(resource.getStructureId(), resource);
580        }
581
582        return result;
583    }
584
585    /**
586     * Rewrites the links included in the content itself.<p>
587     *
588     * @param file the file for which the links should be replaced
589     * @param relations the original relations
590     *
591     * @throws CmsException if something goes wrong
592     */
593    protected void rewriteContent(CmsFile file, Collection<CmsRelation> relations) throws CmsException {
594
595        LOG.info("Rewriting in-content links for " + file.getRootPath());
596        CmsPair<String, String> contentAndEncoding = decode(file);
597        String content = contentAndEncoding.getFirst();
598        String encodingForSave = contentAndEncoding.getSecond();
599        String newContent = rewriteContentString(content);
600        byte[] newContentBytes;
601        try {
602            newContentBytes = newContent.getBytes(encodingForSave);
603        } catch (UnsupportedEncodingException e) {
604            newContentBytes = newContent.getBytes();
605        }
606        file.setContents(newContentBytes);
607        m_cms.writeFile(file);
608    }
609
610    /**
611     * Replaces structure ids of resources in the source subtree with the structure ids of the corresponding
612     * resources in the target subtree inside a content string.<p>
613     *
614     * @param originalContent the original content
615     *
616     * @return the content with the new structure ids
617     */
618    protected String rewriteContentString(String originalContent) {
619
620        Pattern uuidPattern = Pattern.compile(CmsUUID.UUID_REGEX);
621        I_CmsRegexSubstitution substitution = new I_CmsRegexSubstitution() {
622
623            public String substituteMatch(String text, Matcher matcher) {
624
625                String uuidString = text.substring(matcher.start(), matcher.end());
626                CmsUUID uuid = new CmsUUID(uuidString);
627                String result = uuidString;
628                if (m_translationsById.containsKey(uuid)) {
629                    result = m_translationsById.get(uuid).getStructureId().toString();
630                }
631                return result;
632            }
633        };
634        return CmsStringUtil.substitute(uuidPattern, originalContent, substitution);
635    }
636
637    /**
638     * Rewrites the links for a single resource.<p>
639     *
640     * @param resource the resource for which the links should be rewritten
641     * @param relations the relations to the source folder which have this resource as its source
642     *
643     * @throws CmsException if something goes wrong
644     */
645    protected void rewriteLinks(CmsResource resource, Collection<CmsRelation> relations) throws CmsException {
646
647        LOG.info("Rewriting relations for resource " + resource.getRootPath());
648        I_CmsResourceType resourceType = OpenCms.getResourceManager().getResourceType(resource.getTypeId());
649        boolean hasContentLinks = false;
650        boolean hasOtherLinks = false;
651
652        for (CmsRelation relation : relations) {
653            if (relation.getType().isDefinedInContent()) {
654                hasContentLinks = true;
655            } else {
656                hasOtherLinks = true;
657            }
658        }
659        if (hasContentLinks) {
660            LOG.info("The resource " + resource.getRootPath() + " has links in the content.");
661        }
662        if (hasOtherLinks) {
663            LOG.info("The resource " + resource.getRootPath() + " has non-content links.");
664        }
665
666        if (hasContentLinks) {
667            if (resourceType instanceof I_CmsLinkParseable) {
668                CmsFile file = m_cms.readFile(resource);
669                rewriteContent(file, relations);
670                m_rewrittenContent.add(file.getStructureId());
671            }
672        }
673        if (hasOtherLinks) {
674            rewriteOtherRelations(resource, relations);
675        }
676    }
677
678    /**
679     * Rewrites relations which are not derived from links in the content itself.<p>
680     *
681     * @param res the resource for which to rewrite the relations
682     * @param relations the original relations
683     *
684     * @throws CmsException if something goes wrong
685     */
686    protected void rewriteOtherRelations(CmsResource res, Collection<CmsRelation> relations) throws CmsException {
687
688        LOG.info("Rewriting non-content links for " + res.getRootPath());
689        for (CmsRelation rel : relations) {
690            CmsUUID targetId = rel.getTargetId();
691            CmsResource newTargetResource = m_translationsById.get(targetId);
692            CmsRelationType relType = rel.getType();
693            if (!relType.isDefinedInContent()) {
694                if (newTargetResource != null) {
695                    m_cms.deleteRelationsFromResource(
696                        rel.getSourcePath(),
697                        CmsRelationFilter.TARGETS.filterStructureId(rel.getTargetId()).filterType(relType));
698                    m_cms.addRelationToResource(
699                        rel.getSourcePath(),
700                        newTargetResource.getRootPath(),
701                        relType.getName());
702                }
703            }
704        }
705    }
706}