001/*
002 *  Copyright 2010 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.web.repository.page.jcr;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import javax.jcr.ItemExistsException;
027import javax.jcr.Node;
028import javax.jcr.RepositoryException;
029import javax.jcr.Value;
030
031import org.apache.commons.lang.StringUtils;
032
033import org.ametys.plugins.explorer.resources.ResourceCollection;
034import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
035import org.ametys.plugins.repository.AmetysObject;
036import org.ametys.plugins.repository.AmetysObjectIterable;
037import org.ametys.plugins.repository.AmetysRepositoryException;
038import org.ametys.plugins.repository.CopiableAmetysObject;
039import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
040import org.ametys.plugins.repository.RepositoryConstants;
041import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
042import org.ametys.plugins.repository.data.holder.ModifiableDataHolder;
043import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
044import org.ametys.plugins.repository.jcr.SimpleAmetysObject;
045import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
046import org.ametys.runtime.model.exception.BadItemTypeException;
047import org.ametys.runtime.model.exception.NotUniqueTypeException;
048import org.ametys.runtime.model.exception.UndefinedItemPathException;
049import org.ametys.runtime.model.exception.UnknownTypeException;
050import org.ametys.runtime.model.type.DataContext;
051import org.ametys.runtime.model.type.ModelItemTypeConstants;
052import org.ametys.web.parameters.view.ViewParametersManager;
053import org.ametys.web.repository.page.LockablePage;
054import org.ametys.web.repository.page.ModifiableZone;
055import org.ametys.web.repository.page.MoveablePage;
056import org.ametys.web.repository.page.Page;
057import org.ametys.web.repository.page.PageDAO;
058import org.ametys.web.repository.page.SitemapElement;
059import org.ametys.web.repository.site.Site;
060import org.ametys.web.repository.sitemap.Sitemap;
061
062/**
063 * {@link Page} implementation stored into a JCR node using
064 * <code>ametys:defaultPage</code> node type.
065 * 
066 * @param <F> the actual type of factory.
067 */
068public class DefaultPage<F extends DefaultPageFactory> extends AbstractSitemapElement<F> implements LockablePage, CopiableAmetysObject, MoveablePage
069{
070    /** Constant for template metadata. */
071    public static final String METADATA_TEMPLATE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":template";
072
073    /** Constant for title metadata. */
074    public static final String METADATA_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title";
075
076    /** Constant for the initial title metadata. */
077    public static final String METADATA_INITIAL_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title-initial";
078
079    /** Constant for long title metadata. */
080    public static final String METADATA_LONG_TITLE = "long-title";
081
082    /** Constant for title metadata. */
083    public static final String METADATA_TYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":type";
084
085    /** Constant for title metadata. */
086    public static final String METADATA_URL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url";
087
088    /** Constant for title metadata. */
089    public static final String METADATA_URLTYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":url-type";
090
091    /** Constant for referers metadata. */
092    public static final String METADATA_REFERERS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":referers";
093    
094    /** Constant for the visible attribute. */
095    public static final String METADATA_VISIBLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":visible";
096    
097    /** Constant for the locked attribute. */
098    public static final String METADATA_LOCKED = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":locked";
099
100    /** Constant for robots metadata. */
101    public static final String METADATA_ROBOTS_DISALLOW = "robots-disallow";
102    
103    /** Constant for the attachment node name. */
104    public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments";
105    
106    /** Constants for sitemap Metadata* */
107    public static final String METADATA_SITEMAP = "sitemap";
108
109    /** Constant for publication start date metadata. */
110    public static final String METADATA_PUBLICATION_START_DATE = "publicationStartDate";
111    
112    /** Constant for publication end date metadata. */
113    public static final String METADATA_PUBLICATION_END_DATE = "publicationEndDate";
114    
115    /**
116     * Creates a {@link DefaultPage}.
117     * 
118     * @param node the node backing this {@link AmetysObject}.
119     * @param parentPath the parent path in the Ametys hierarchy.
120     * @param factory the {@link DefaultPageFactory} which creates the
121     *            AmetysObject.
122     */
123    public DefaultPage(Node node, String parentPath, F factory)
124    {
125        super(node, parentPath, factory);
126    }
127    
128    @Override
129    public String getTemplate() throws AmetysRepositoryException
130    {
131        if (getType() != PageType.CONTAINER)
132        {
133            return null;
134        }
135        
136        try
137        {
138            Node node = getNode();
139            
140            if (node.hasProperty(METADATA_TEMPLATE))
141            {
142                return node.getProperty(METADATA_TEMPLATE).getString();
143            }
144            else
145            {
146                return null;
147            }
148        }
149        catch (RepositoryException e)
150        {
151            throw new AmetysRepositoryException("Unable to get template property", e);
152        }
153    }
154
155    @Override
156    public void setTemplate(String template) throws AmetysRepositoryException
157    {
158        try
159        {
160            getNode().setProperty(METADATA_TEMPLATE, template);
161        }
162        catch (RepositoryException e)
163        {
164            throw new AmetysRepositoryException("Unable to set template property", e);
165        }
166    }
167
168    @Override
169    public Set<String> getTags() throws AmetysRepositoryException
170    {
171        return TaggableAmetysObjectHelper.getTags(this);
172    }
173    
174    @Override
175    public void tag(String tag) throws AmetysRepositoryException
176    {
177        TaggableAmetysObjectHelper.tag(this, tag);
178    }
179
180    @Override
181    public void untag(String tag) throws AmetysRepositoryException
182    {
183        TaggableAmetysObjectHelper.untag(this, tag);
184    }
185
186    @Override
187    public String getTitle() throws AmetysRepositoryException
188    {
189        try
190        {
191            return getNode().getProperty(METADATA_TITLE).getString();
192        }
193        catch (RepositoryException e)
194        {
195            throw new AmetysRepositoryException("Unable to get title property", e);
196        }
197    }
198    
199    @Override
200    public void setTitle(String title) throws AmetysRepositoryException
201    {
202        try
203        {
204            getNode().setProperty(METADATA_TITLE, title);
205        }
206        catch (RepositoryException e)
207        {
208            throw new AmetysRepositoryException("Unable to set title property", e);
209        }
210        setInitialTitle(title);
211    }
212
213    /**
214     * Get the title that was set by the user (and not set by a copy adding a number after)
215     * @return The initial title
216     * @throws AmetysRepositoryException if an error occurs
217     */
218    public String getInitialTitle()  throws AmetysRepositoryException
219    {
220        try
221        {
222            if (getNode().hasProperty(METADATA_INITIAL_TITLE))
223            {
224                return getNode().getProperty(METADATA_INITIAL_TITLE).getString();
225            }
226            else
227            {
228                return getTitle();
229            }
230        }
231        catch (RepositoryException e)
232        {
233            throw new AmetysRepositoryException("Unable to get initial title property", e);
234        }
235    }
236    
237    /**
238     * Set the title that was set by the user (and not set by a copy adding a number after)
239     * @param initialTitle The initial title to set
240     * @throws AmetysRepositoryException if an error occurs
241     */
242    public void setInitialTitle(String initialTitle) throws AmetysRepositoryException
243    {
244        try
245        {
246            getNode().setProperty(METADATA_INITIAL_TITLE, initialTitle);
247        }
248        catch (RepositoryException e)
249        {
250            throw new AmetysRepositoryException("Unable to set initial title property", e);
251        }
252    }
253
254    @Override
255    public String getLongTitle() throws AmetysRepositoryException
256    {
257        String longTitle = getValue(METADATA_LONG_TITLE);
258        if (StringUtils.isNotBlank(longTitle))
259        {
260            return longTitle;
261        }
262        else
263        {
264            return this.getTitle();
265        }
266    }
267
268    @Override
269    public void setLongTitle(String title) throws AmetysRepositoryException
270    {
271        String newTitle = StringUtils.trimToNull(title);
272        setValue(METADATA_LONG_TITLE, newTitle, ModelItemTypeConstants.STRING_TYPE_ID);
273    }
274
275    @Override
276    public Sitemap getSitemap() throws AmetysRepositoryException
277    {
278        try
279        {
280            Node node = getNode();
281
282            do
283            {
284                node = node.getParent();
285            }
286            while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE));
287
288            return _getFactory().resolveAmetysObject(node);
289        }
290        catch (RepositoryException e)
291        {
292            throw new AmetysRepositoryException("Unable to get sitemap", e);
293        }
294    }
295    
296    @Override
297    public int getDepth() throws AmetysRepositoryException
298    {
299        try
300        {
301            int depth = 1;
302            Node node = getNode().getParent();
303            while (!node.getPrimaryNodeType().getName().equals(Sitemap.NODE_TYPE))
304            {
305                depth++;
306                node = node.getParent();
307            }
308
309            return depth;
310        }
311        catch (RepositoryException e)
312        {
313            throw new AmetysRepositoryException("Unable to get depth", e);
314        }
315    }
316
317    @Override
318    public Site getSite() throws AmetysRepositoryException
319    {
320        return getSitemap().getSite();
321    }
322    
323    @Override
324    public String getSiteName()
325    {
326        return getValue(METADATA_SITE);
327    }
328    
329    @Override
330    public void setSiteName(String siteName)
331    {
332        setValue(METADATA_SITE, siteName);
333    }
334    
335    @Override
336    public String getSitemapName() throws AmetysRepositoryException
337    {
338        return getValue(METADATA_SITEMAP);
339    }
340    
341    @Override
342    public void setSitemapName(String sitemapName) throws AmetysRepositoryException
343    {
344        setValue(METADATA_SITEMAP, sitemapName);
345    }
346
347    @Override
348    public String getPathInSitemap() throws AmetysRepositoryException
349    {
350        String nodePath = getPath();
351        String sitemapPath = getSitemap().getPath();
352        return nodePath.substring(sitemapPath.length() + 1);
353    }
354
355    @Override
356    public PageType getType() throws AmetysRepositoryException
357    {
358        try
359        {
360            return PageType.valueOf(getNode().getProperty(METADATA_TYPE).getString());
361        }
362        catch (RepositoryException e)
363        {
364            throw new AmetysRepositoryException("Unable to get type property", e);
365        }
366    }
367
368    @Override
369    public void setType(PageType type) throws AmetysRepositoryException
370    {
371        try
372        {
373            getNode().setProperty(METADATA_TYPE, type.name());
374        }
375        catch (RepositoryException e)
376        {
377            throw new AmetysRepositoryException("Unable to set type property", e);
378        }
379    }
380
381    @Override
382    public String getURL() throws AmetysRepositoryException
383    {
384        try
385        {
386            return getNode().getProperty(METADATA_URL).getString();
387        }
388        catch (RepositoryException e)
389        {
390            throw new AmetysRepositoryException("Unable to get url property", e);
391        }
392    }
393    
394    @Override
395    public LinkType getURLType() throws AmetysRepositoryException
396    {
397        try
398        {
399            return LinkType.valueOf(getNode().getProperty(METADATA_URLTYPE).getString());
400        }
401        catch (RepositoryException e)
402        {
403            throw new AmetysRepositoryException("Unable to get url type property", e);
404        }
405    }
406
407    @Override
408    public void setURL(LinkType type, String url) throws AmetysRepositoryException
409    {
410        try
411        {
412            getNode().setProperty(METADATA_URLTYPE, type.toString());
413            getNode().setProperty(METADATA_URL, url);
414        }
415        catch (RepositoryException e)
416        {
417            throw new AmetysRepositoryException("Unable to set url property", e);
418        }
419    }
420
421    @Override
422    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
423    {
424        ResourceCollection attachments;
425        if (hasChild(ATTACHMENTS_NODE_NAME))
426        {
427            attachments = getChild(ATTACHMENTS_NODE_NAME);
428        }
429        else
430        {
431            attachments = createChild(ATTACHMENTS_NODE_NAME, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE);
432            
433            try
434            {
435                getNode().getSession().save();
436            }
437            catch (RepositoryException e)
438            {
439                throw new AmetysRepositoryException("Unable to save session", e);
440            }
441        }
442        
443        return attachments;
444    }
445    
446    @Override
447    public Set<String> getReferers() throws AmetysRepositoryException
448    {
449        try
450        {
451            Node node = getNode();
452
453            if (!node.hasProperty(METADATA_REFERERS))
454            {
455                return Collections.emptySet();
456            }
457
458            Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
459            Set<String> ids = new HashSet<>(values.length);
460
461            for (Value value : values)
462            {
463                ids.add(value.getString());
464            }
465
466            return ids;
467        }
468        catch (RepositoryException e)
469        {
470            throw new AmetysRepositoryException("Unable to get referers property", e);
471        }
472    }
473
474    @Override
475    public void addReferer(String ametysObjectId) throws AmetysRepositoryException
476    {
477        try
478        {
479            Set<String> ids = null;
480            Node node = getNode();
481
482            if (!node.hasProperty(METADATA_REFERERS))
483            {
484                ids = Collections.singleton(ametysObjectId);
485            }
486            else
487            {
488                Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
489                ids = new HashSet<>(values.length + 1);
490
491                for (Value value : values)
492                {
493                    ids.add(value.getString());
494                }
495
496                ids.add(ametysObjectId);
497            }
498
499            node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()]));
500        }
501        catch (RepositoryException e)
502        {
503            throw new AmetysRepositoryException("Unable to add an id into referers property", e);
504        }
505    }
506
507    @Override
508    public void removeReferer(String ametysObjectId) throws AmetysRepositoryException
509    {
510        try
511        {
512            Node node = getNode();
513
514            if (node.hasProperty(METADATA_REFERERS))
515            {
516                Value[] values = getNode().getProperty(METADATA_REFERERS).getValues();
517                Set<String> ids = new HashSet<>(values.length + 1);
518
519                for (Value value : values)
520                {
521                    ids.add(value.getString());
522                }
523
524                ids.remove(ametysObjectId);
525                node.setProperty(METADATA_REFERERS, ids.toArray(new String[ids.size()]));
526            }
527        }
528        catch (RepositoryException e)
529        {
530            throw new AmetysRepositoryException("Unable to remove an id from referers property", e);
531        }
532    }
533    
534    @Override
535    public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
536    {
537        try
538        {
539            String pageName = PageDAO.findFreePageName(parent, name != null ? name : getName());
540            
541            DefaultPage cPage = parent.createChild(pageName, "ametys:defaultPage");
542            cPage.setType(getType());
543            cPage.setTitle(getTitle());
544            cPage.setInitialTitle(getInitialTitle());
545            
546            if (PageType.CONTAINER.equals(getType()))
547            {
548                cPage.setTemplate(getTemplate());
549            }
550            else if (PageType.LINK.equals(getType()))
551            {
552                cPage.setURL(getURLType(), getURL());
553            }
554            
555            // Copy metadata
556            copyTo(cPage);
557            
558            // Copy view parameters
559            getTemplateParametersHolder().copyTo(cPage.getTemplateParametersHolder());
560            
561            // Copy zones
562            for (ModifiableZone zone : getZones())
563            {
564                if (zone instanceof CopiableAmetysObject)
565                {
566                    ((CopiableAmetysObject) zone).copyTo(cPage, null);
567                }
568            }
569            
570            // Copy tags
571            Set<String> tags = getTags();
572            for (String tag : tags)
573            {
574                cPage.tag(tag);
575            }
576            
577            // Update sitemap name
578            if (parent instanceof SitemapElement)
579            {
580                cPage.setSitemapName(((SitemapElement) parent).getSitemapName());
581            }
582            
583            parent.saveChanges();
584            
585            // Copy attachments
586            ResourceCollection rootAttachments = getRootAttachments();
587            if (rootAttachments instanceof SimpleAmetysObject)
588            {
589                Node resourcesNode = ((SimpleAmetysObject) rootAttachments).getNode();
590                getNode().getSession().getWorkspace().copy(resourcesNode.getPath(), cPage.getNode().getPath() + "/" + resourcesNode.getName());
591            }
592            
593            // Copy sub-pages
594            AmetysObjectIterable< ? extends Page> childrenPages = getChildrenPages();
595            for (Page page : childrenPages)
596            {
597                // Avoid infinite loop : do not copy the page itself
598                if (page instanceof CopiableAmetysObject && restrictTo.contains(page.getId()))
599                {
600                    ((CopiableAmetysObject) page).copyTo(cPage, null, restrictTo);
601                }
602            }
603            
604            return cPage;
605        }
606        catch (RepositoryException e)
607        {
608            throw new AmetysRepositoryException(e);
609        }
610    }
611    
612    @Override
613    public DefaultPage copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
614    {
615        return copyTo (parent, name, new ArrayList<>());
616    }
617    
618    public void copyTo(ModifiableDataHolder dataHolder) throws UndefinedItemPathException, BadItemTypeException, UnknownTypeException, NotUniqueTypeException
619    {
620        for (String name : getDataNames())
621        {
622            if (!_getExcludedDataFromCopy().contains(name))
623            {
624                DataHolderHelper.copyTo(getDataHolder(), dataHolder, name, DataContext.newInstance());
625            }
626        }
627    }
628    
629    /**
630     * Get the set of data name to exclude from the copy
631     * @return the set of data name to exclude from the copy
632     */
633    protected Set<String> _getExcludedDataFromCopy()
634    {
635        return Set.of(
636            ViewParametersManager.VIEW_PARAMETERS_COMPOSITE_NAME,
637            METADATA_PUBLICATION_START_DATE,
638            METADATA_PUBLICATION_END_DATE
639        );
640    }
641    
642    @Override
643    public void moveTo(AmetysObject newParent, boolean renameIfExist) throws AmetysRepositoryException, RepositoryIntegrityViolationException
644    {
645        Node node = getNode();
646
647        try
648        {
649            if (getParent().equals(newParent))
650            {
651                // Just order current node to the end
652                node.getParent().orderBefore(node.getName(), null);
653            }
654            else
655            {
656                if (!canMoveTo(newParent))
657                {
658                    throw new AmetysRepositoryException("DefaultPage " + toString() + " can only be moved to a CompositePages and ");
659                }
660                SitemapElement newParentPage = (SitemapElement) newParent;
661                
662                String sitemapNodePath = getSitemap().getNode().getPath();
663                String newPath = sitemapNodePath + (newParentPage instanceof Sitemap ? "" : '/' + newParentPage.getPathInSitemap());
664
665                String name = node.getName();
666                
667                if (renameIfExist)
668                {
669                    // Find unused name on new parent node
670                    int index = 1;
671                    while (node.getSession().getRootNode().hasNode(newPath.substring(1) + "/" + name))
672                    {
673                        name = name + "-" + index++;
674                    }
675                }
676
677                try
678                {
679                    // Move node
680                    node.getSession().move(node.getPath(), newPath + "/" + name);
681                }
682                catch (ItemExistsException e)
683                {
684                    throw new AmetysRepositoryException(String.format("A page already exists for in parent path '%s'", newPath), e);
685                }
686                
687                // recompute name in case it has changed
688                _invalidateName();
689
690                // Invalidate parent path as the parent path has changed
691                _invalidateParentPath();
692            }
693        }
694        catch (RepositoryException e)
695        {
696            throw new AmetysRepositoryException(String.format("Unable to move page '%s' to node '%s'", this, newParent.getId()), e);
697        }
698    }
699    
700    @Override
701    public boolean canMoveTo(AmetysObject newParent) throws AmetysRepositoryException
702    {
703        return  newParent instanceof SitemapElement && ((SitemapElement) newParent).getSitemap().equals(getSitemap());
704    }
705
706    @Override
707    public void orderBefore(AmetysObject siblingNode) throws AmetysRepositoryException
708    {
709        Node node = getNode();
710        try
711        {
712            node.getParent().orderBefore(node.getName(), siblingNode != null ? siblingNode.getName() : null);
713        }
714        catch (RepositoryException e)
715        {
716            throw new AmetysRepositoryException(String.format("Unable to order page '%s' before sibling '%s'", this, siblingNode != null ? siblingNode.getName() : ""), e);
717        }
718    }
719
720    @Override
721    public boolean isVisible() throws AmetysRepositoryException
722    {
723        try
724        {
725            if (getNode().hasProperty(METADATA_VISIBLE))
726            {
727                return getNode().getProperty(METADATA_VISIBLE).getBoolean();
728            }
729            else
730            {
731                return true;
732            }
733        }
734        catch (RepositoryException e)
735        {
736            throw new AmetysRepositoryException("Unable to get visible property", e);
737        }
738    }
739    
740    @Override
741    public void setVisible(boolean isVisible) throws AmetysRepositoryException
742    {
743        try
744        {
745            getNode().setProperty(METADATA_VISIBLE, isVisible);
746        }
747        catch (RepositoryException e)
748        {
749            throw new AmetysRepositoryException("Unable to set visible property", e);
750        }
751    }
752    
753    public Map<String, Object> dataToJSON(DataContext context) throws UnknownTypeException, NotUniqueTypeException
754    {
755        Map<String, Object> result = new HashMap<>();
756        for (String dataName : getDataNames())
757        {
758            DataContext newContext = context.cloneContext().addSegmentToDataPath(dataName);
759            result.put(dataName, dataToJSON(dataName, newContext));
760        }
761        
762        return result;
763    }
764    
765    public Object dataToJSON(String dataPath, DataContext context)
766    {
767        // Do not convert view parameters
768        return !ViewParametersManager.VIEW_PARAMETERS_COMPOSITE_NAME.equals(dataPath)
769                ? LockablePage.super.dataToJSON(dataPath, context)
770                : null;
771    }
772
773    public boolean isLocked() throws AmetysRepositoryException
774    {
775        try
776        {
777            if (getNode().hasProperty(METADATA_LOCKED))
778            {
779                return getNode().getProperty(METADATA_LOCKED).getBoolean();
780            }
781            else
782            {
783                return false;
784            }
785        }
786        catch (RepositoryException e)
787        {
788            throw new AmetysRepositoryException("Unable to get locked property", e);
789        }
790    }
791
792    public void lock() throws AmetysRepositoryException
793    {
794        try
795        {
796            getNode().setProperty(METADATA_LOCKED, true);
797        }
798        catch (RepositoryException e)
799        {
800            throw new AmetysRepositoryException("Unable to set locked property", e);
801        }
802    }
803
804    public void unlock() throws AmetysRepositoryException
805    {
806        try
807        {
808            getNode().setProperty(METADATA_LOCKED, false);
809        }
810        catch (RepositoryException e)
811        {
812            throw new AmetysRepositoryException("Unable to set locked property", e);
813        }
814    }
815}