Package de.innovationgate.webgate.api

Source Code of de.innovationgate.webgate.api.WGContent$SessionData

/*******************************************************************************
* Copyright 2009, 2010 Innovation Gate GmbH. All Rights Reserved.
*
* This file is part of the OpenWGA server platform.
*
* OpenWGA is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition, a special exception is granted by the copyright holders
* of OpenWGA called "OpenWGA plugin exception". You should have received
* a copy of this exception along with OpenWGA in file COPYING.
* If not, see <http://www.openwga.com/gpl-plugin-exception>.
*
* OpenWGA is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenWGA in file COPYING.
* If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
package de.innovationgate.webgate.api;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import de.innovationgate.utils.NullPlaceHolder;
import de.innovationgate.utils.TemporaryFile;
import de.innovationgate.utils.UIDGenerator;
import de.innovationgate.utils.WGUtils;
import de.innovationgate.utils.XStreamUtils;
import de.innovationgate.webgate.api.WGDatabase.FreeContentVersionFinder;
import de.innovationgate.webgate.api.WGSessionContext.DocumentContext;
import de.innovationgate.webgate.api.auth.AuthenticationModule;
import de.innovationgate.webgate.api.fake.WGFakeStructEntry;
import de.innovationgate.webgate.api.locking.Lockable;
import de.innovationgate.webgate.api.workflow.WGWorkflow;

/**
*
* Represents a content document of this content database.
*/
public class WGContent extends WGDocument implements PageHierarchyNode {
   
    private static final String STORE_AUTHOR_EMAIL = "AuthorEMail";
    public static final String SYSPROPERTY_LUCENE_KEYWORDS_AS_CONTENT = "de.innovationgate.wga.lucene.keywords_as_content";

    public class SessionData {
        private boolean saveEventSemaphore = false;
        private Float searchScore = null;
        private Object searchExplaination = null;
        private boolean performStatusTestsOnSave = false;
        private Map virtualItems = new HashMap();
        private String backendStatus = null;
       
        public boolean isSaveEventSemaphore() {
            return saveEventSemaphore;
        }
        public void setSaveEventSemaphore(boolean saveEventSemaphore) {
            this.saveEventSemaphore = saveEventSemaphore;
        }
        public Float getSearchScore() {
            return searchScore;
        }
        public void setSearchScore(Float searchScore) {
            this.searchScore = searchScore;
        }
        public Object getSearchExplaination() {
            return searchExplaination;
        }
        public void setSearchExplaination(Object searchExplaination) {
            this.searchExplaination = searchExplaination;
        }
        public boolean isPerformStatusTestsOnSave() {
            return performStatusTestsOnSave;
        }
        public void setPerformStatusTestsOnSave(boolean bypassStatusTests) {
            this.performStatusTestsOnSave = bypassStatusTests;
        }
        protected Map getVirtualItems() {
            return virtualItems;
        }
        protected void setVirtualItems(Map virtualItems) {
            this.virtualItems = virtualItems;
        }
        public String getBackendStatus() {
            return backendStatus;
        }
        public void setBackendStatus(String backendStatus) {
            this.backendStatus = backendStatus;
        }
       
    }

   
   
    /**
     * Content is in status draft can be edited.
     */
    public static final String STATUS_DRAFT = "w";
    /**
     * Content is reviewed by workflow approvers
     */
    public static final String STATUS_REVIEW = "g";
    /**
     * Content is released and shown online
     */
    public static final String STATUS_RELEASE = "p";
    /**
     * Document is archived and no longer used.
     */
    public static final String STATUS_ARCHIVE = "a";
   
    /**
     * Item to store the replace reason comment (workflow).
     */
    public static final String ITEM_REPLACEREASON = "Ersetzungsgrund";
   
    /**
     * Content is hidden for navigators.
     */
    public static final String DISPLAYTYPE_NAVIGATOR = "nav";
    /**
     * Content is invisible for searches
     */
    public static final String DISPLAYTYPE_SEARCH = "search";
    /**
     * Content is invisible in sitemaps.
     */
    public static final String DISPLAYTYPE_SITEMAP = "sitemap";
    /**
     * Content is not hidden for any navigation facility.
     * (Not to be used in setHiddenFrom() but in other occurences, where the DISPLAYTYPE constants are used to specify which content should be filtered)
     */
    public static final String DISPLAYTYPE_NONE = "none";

    /**
     * Virtual link to another content in this database
     */
    public static final String VIRTUALLINKTYPE_CONTENT = "int";
    /**
     * Virtual link to a file on some file container
     */
    public static final String VIRTUALLINKTYPE_FILE = "file";
    /**
     * Virtual link to an attachment file in this document
     */
    public static final String VIRTUALLINKTYPE_INTERNAL_FILE = "intfile";
    /**
     * Virtual link to any URL
     */
    public static final String VIRTUALLINKTYPE_EXTERNAL = "exturl";
    /**
     * Link type was not specified.
     */
    public static final String VIRTUALLINKTYPE_NOT_SPECIFIED = "";
    /**
     * Virtual link to a document with unique name
     */
    public static final String VIRTUALLINKTYPE_UNIQUENAME = "intname";
    /**
     * Virtual link to a file on another content document
     */
    public static final String VIRTUALLINKTYPE_EXTERNAL_FILE = "extfile";
   

    public static final DateFormat DATEFORMAT_HISTORY = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");

    /**
     * Used with WGContentNavigator to determine ascending searchorder when finding content
     */
    public static final int SEARCHORDER_ASCENDING = 1;
    /**
     * Used with WGContentNavigator to determine descending searchorder when finding content
     */
    public static final int SEARCHORDER_DESCENDING = -1;
   
    /**
     * Relation type that will allow deletion of targeted documents without further notice
     */
    public static final int RELATIONTYPE_NORMAL = 1;
   
    /**
     * Relation type that will deny deletion of targeted documents
     */
    public static final int RELATIONTYPE_PROTECTED = 2;

  public static final String META_STRUCTENTRY = "STRUCTENTRY";
    public static final MetaInfo METAINFO_STRUCTENTRY = new MetaInfo(META_STRUCTENTRY, Object.class, null);
    static { METAINFO_STRUCTENTRY.addSynonym("STRUCTKEY"); };
   
  public static final String META_TITLE = "TITLE";
    public static final MetaInfo METAINFO_TITLE = new MetaInfo(META_TITLE, String.class, "");
    static {
      METAINFO_TITLE.setLuceneAddToAllContent(true);
      METAINFO_TITLE.setLuceneBoost(2);
      METAINFO_TITLE.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT);
     
      METAINFO_TITLE.setInputConverter(TITLE_CONVERTER);
      METAINFO_TITLE.setOutputConverter(TITLE_CONVERTER);   
    }
   
  public static final String META_LANGUAGE = "LANGUAGE";
    public static final MetaInfo METAINFO_LANGUAGE = new MetaInfo(META_LANGUAGE, String.class, null);
    static { METAINFO_LANGUAGE.setLowerCase(true); };
   
  public static final String META_STATUS = "STATUS";
    public static final MetaInfo METAINFO_STATUS = new MetaInfo(META_STATUS, String.class, null);
    static {
        METAINFO_STATUS.addAllowedValue(STATUS_ARCHIVE);
        METAINFO_STATUS.addAllowedValue(STATUS_DRAFT);
        METAINFO_STATUS.addAllowedValue(STATUS_RELEASE);
        METAINFO_STATUS.addAllowedValue(STATUS_REVIEW);
       
    };
   
  public static final String META_VERSION = "VERSION";
    public static final MetaInfo METAINFO_VERSION = new MetaInfo(META_VERSION, Integer.class, null);
   
  public static final String META_UNIQUE_NAME = "UNIQUENAME";
    public static final MetaInfo METAINFO_UNIQUE_NAME = new MetaInfo(META_UNIQUE_NAME, String.class, null);
    static { METAINFO_UNIQUE_NAME.setLowerCase(true); METAINFO_UNIQUE_NAME.addSynonym("NAME"); METAINFO_UNIQUE_NAME.addSynonym("DOCNAME"); };
   
  public static final String META_IS_HIDDEN_FROM = "ISHIDDENFROM";
    public static final MetaInfo METAINFO_IS_HIDDEN_FROM = new MetaInfo(META_IS_HIDDEN_FROM, String.class, Collections.EMPTY_LIST);
    static {
        METAINFO_IS_HIDDEN_FROM.setMultiple(true);
        METAINFO_IS_HIDDEN_FROM.setLuceneSpecialTreatment(true);
        METAINFO_IS_HIDDEN_FROM.addAllowedValue(DISPLAYTYPE_NAVIGATOR);
        METAINFO_IS_HIDDEN_FROM.addAllowedValue(DISPLAYTYPE_SEARCH);
        METAINFO_IS_HIDDEN_FROM.addAllowedValue(DISPLAYTYPE_SITEMAP);
    };
   
  public static final String META_VISIBLE = "VISIBLE";
    public static final MetaInfo METAINFO_VISIBLE = new MetaInfo(META_VISIBLE, Boolean.class, Boolean.TRUE);
   
  public static final String META_VALID_FROM = "VALIDFROM";
    public static final MetaInfo METAINFO_VALID_FROM = new MetaInfo(META_VALID_FROM, Date.class, null);
    static { METAINFO_VALID_FROM.setLuceneSpecialTreatment(true); };
   
  public static final String META_VALID_TO = "VALIDTO";
    public static final MetaInfo METAINFO_VALID_TO = new MetaInfo(META_VALID_TO, Date.class, null);
    static { METAINFO_VALID_TO.setLuceneSpecialTreatment(true); };
   
  public static final String META_CONTENTCLASS = "CONTENTCLASS";
    public static final MetaInfo METAINFO_CONTENTCLASS = new MetaInfo(META_CONTENTCLASS, String.class, null);
    static {
        METAINFO_CONTENTCLASS.setMinCsVersion(WGDatabase.CSVERSION_WGA5);
    }
   
  public static final String META_VIRTUAL_LINK = "VIRTUALLINK";
    public static final MetaInfo METAINFO_VIRTUAL_LINK = new MetaInfo(META_VIRTUAL_LINK, String.class, null);
    static {
        METAINFO_VIRTUAL_LINK.setExtdataSinceCsVersion(WGDatabase.CSVERSION_WGA5);
        METAINFO_VIRTUAL_LINK.setOutputConverter(new MetaConverter() {

            public Object convert(WGDocument doc, MetaInfo metaInfo, Object value) throws WGAPIException {
                if (!(doc instanceof WGContent)) {                   
                    throw new WGIllegalArgumentException("Unable to execute output converter for meta '" + metaInfo.getName() + "'. Given document is no instance of WGContent.");
                } else {
                    WGContent content = (WGContent) doc;
                    String link = (String) value;
                    if (content.getVirtualLinkType().equals(WGContent.VIRTUALLINKTYPE_INTERNAL_FILE) && link.indexOf("/") != -1) {
                        link = link.substring(link.lastIndexOf("/") + 1);
                    }
                    return link;
                }
            }  
        });
       
    };
   
  public static final String META_VIRTUAL_LINK_TYPE = "VIRTUALLINKTYPE";
    public static final MetaInfo METAINFO_VIRTUAL_LINK_TYPE = new MetaInfo(META_VIRTUAL_LINK_TYPE, String.class, VIRTUALLINKTYPE_EXTERNAL);
    static {
        METAINFO_VIRTUAL_LINK_TYPE.setExtdataSinceCsVersion(WGDatabase.CSVERSION_WGA5);
        METAINFO_VIRTUAL_LINK_TYPE.addAllowedValue(VIRTUALLINKTYPE_CONTENT);
        METAINFO_VIRTUAL_LINK_TYPE.addAllowedValue(VIRTUALLINKTYPE_EXTERNAL);
        METAINFO_VIRTUAL_LINK_TYPE.addAllowedValue(VIRTUALLINKTYPE_FILE);
        METAINFO_VIRTUAL_LINK_TYPE.addAllowedValue(VIRTUALLINKTYPE_INTERNAL_FILE);
        METAINFO_VIRTUAL_LINK_TYPE.addAllowedValue(VIRTUALLINKTYPE_EXTERNAL_FILE);
        METAINFO_VIRTUAL_LINK_TYPE.addAllowedValue(VIRTUALLINKTYPE_UNIQUENAME);
        METAINFO_VIRTUAL_LINK_TYPE.addAllowedValue(VIRTUALLINKTYPE_NOT_SPECIFIED);
    };
   
  public static final String META_LINK_TARGET = "LINKTARGET";
    public static final MetaInfo METAINFO_LINK_TARGET = new MetaInfo(META_LINK_TARGET, String.class, null);
      
  public static final String META_KEYWORDS = "KEYWORDS";
    public static final MetaInfo METAINFO_KEYWORDS = new MetaInfo(META_KEYWORDS, String.class, Collections.EMPTY_LIST);
    static {
        METAINFO_KEYWORDS.setMultiple(true);
        METAINFO_KEYWORDS.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT);
        if ("true".equals(System.getProperty(SYSPROPERTY_LUCENE_KEYWORDS_AS_CONTENT))) {
            METAINFO_KEYWORDS.setLuceneAddToAllContent(true);
        }
    };
   
  public static final String META_AUTHOR = "AUTHOR";
    public static final MetaInfo METAINFO_AUTHOR = new MetaInfo(META_AUTHOR, String.class, null);
    static { METAINFO_AUTHOR.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT); };
   
    public static final String META_OWNER = "OWNER";
    public static final MetaInfo METAINFO_OWNER = new MetaInfo(META_OWNER, String.class, null);
    static {
        METAINFO_OWNER.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT);
        METAINFO_OWNER.setMinCsVersion(WGDatabase.CSVERSION_WGA5);
    };

    public static final String META_COAUTHORS = "COAUTHORS";
    public static final MetaInfo METAINFO_COAUTHORS = new MetaInfo(META_COAUTHORS, String.class, Collections.EMPTY_LIST);
    static {
        METAINFO_COAUTHORS.setMultiple(true);
        METAINFO_COAUTHORS.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT);
        METAINFO_COAUTHORS.setMinCsVersion(WGDatabase.CSVERSION_WGA5);
    };
   
  public static final String META_SEARCHSCORE = "SEARCHSCORE";
    public static final MetaInfo METAINFO_SEARCHSCORE = new MetaInfo(META_SEARCHSCORE, Integer.class, new Integer(0));
    static { METAINFO_SEARCHSCORE.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_NOINDEX); };
   
  public static final String META_WFHISTORY = "WFHISTORY";
    public static final MetaInfo METAINFO_WFHISTORY = new MetaInfo(META_WFHISTORY, String.class, Collections.EMPTY_LIST);
    static { METAINFO_WFHISTORY.setMultiple(true); METAINFO_WFHISTORY.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT); };
   
  public static final String META_DESCRIPTION = "DESCRIPTION";
    public static final MetaInfo METAINFO_DESCRIPTION = new MetaInfo(META_DESCRIPTION, String.class, "");
    static {
        METAINFO_DESCRIPTION.setExtdataSinceCsVersion(WGDatabase.CSVERSION_WGA5);
        METAINFO_DESCRIPTION.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT);
        METAINFO_DESCRIPTION.setLuceneBoost((float)1.5);
        METAINFO_DESCRIPTION.setLuceneAddToAllContent(true);
    };
   
  public static final String META_READERS = "READERS";
    public static final MetaInfo METAINFO_READERS = new MetaInfo(META_READERS, String.class, Collections.EMPTY_LIST);
    static {
        METAINFO_READERS.setMultiple(true);
        METAINFO_READERS.setLuceneIndexType(MetaInfo.LUCENE_INDEXTYPE_FULLTEXT);
        METAINFO_READERS.setInputConverter(new MetaConverter() {

            public Object convert(WGDocument doc, MetaInfo metaInfo, Object value) throws WGAPIException {
                    List readers = (List) value;
                   
                    // Test if the current user is contained in the list. If not, he is added to avoid self disclosure
                    if (!WGDatabase.anyoneAllowed(readers) && !doc.getDatabase().isMemberOfUserList(readers)) {
                        readers.add(doc.getDatabase().getSessionContext().getUser());
                    }
                return readers;
            }
           
        });
    };
   
  public static final String META_LASTCLIENT = "LASTCLIENT";
    public static final MetaInfo METAINFO_LASTCLIENT = new MetaInfo(META_LASTCLIENT, String.class, null);
    static {
        METAINFO_LASTCLIENT.setExtdataSinceCsVersion(WGDatabase.CSVERSION_WGA5);
    }
   
    public static final String META_PUBLISHED = "PUBLISHED";
    public static final MetaInfo METAINFO_PUBLISHED = new MetaInfo(META_PUBLISHED, Date.class, null);
    static {
        METAINFO_PUBLISHED.setMinCsVersion(WGDatabase.CSVERSION_WGA5);
    }

    private WGContentKey retrievalContentKey;

  private Map isVisibleFor = Collections.synchronizedMap(new HashMap());


    private String retrievalStatus;
    private String retrievalUniqueName;
    private String retrievalLanguage;
   
    private Set<String> dontCacheTheseRelations = new CopyOnWriteArraySet<String>();

  /**
   * Constructor. Should not be used outside the WGAPI.
   * @throws WGAPIException
   */
  public WGContent(WGDatabase db, WGDocumentCore doc) throws WGAPIException {
    super(db, doc);
   
    // Not neccessary. WGDocument constructor does this by calling dropCache()
        // gatherRetrievalKeys();
       
  }

    /**
     * Gather all keys that must be available, undependent of the cache situation.
     * These will be needed if the document was deleted in background, to unmap
     * this object
     * @throws WGAPIException
     */
    private void gatherRetrievalKeys() throws WGAPIException {
    this.retrievalContentKey = WGContentKey.create(this, true);
        this.retrievalStatus = getStatus();
        this.retrievalUniqueName = getUniqueName();
        this.retrievalLanguage = (String) getMetaData(META_LANGUAGE);
    }

  /**
   * Returns the language of this content document.
   * @return WGLanguage
   * @throws WGAPIException
   */
  public WGLanguage getLanguage() throws WGAPIException {
    return this.db.getLanguage(String.valueOf(this.getMetaData(META_LANGUAGE)));
  }

  /**
   * Returns the title of this content document.
   * @return String
   * @throws WGAPIException
   */
  public String getTitle() throws WGAPIException {
    return this.getMetaData(META_TITLE).toString();
  }

  /**
   * @throws WGAPIException
   * @see WGDocument#dropCache()
   */
  public void dropCache() throws WGAPIException {
    dropRelations();
        if (isVisibleFor != null) {
            this.isVisibleFor.clear();
        }
       
        // Might get called even before WGContent initialisation
        if (dontCacheTheseRelations != null) {
            dontCacheTheseRelations.clear();
        }
       
    super.dropCache();
       
        if (!isDisposed() && !isDeleted()) {
            gatherRetrievalKeys();
        }
  }

  /* (non-Javadoc)
   * @see de.innovationgate.webgate.api.WGDocument#dropRelations()
   */
  protected void dropRelations() {
  }

  /**
   * Returns the struct entry, which this content document is attached to.
   * @return WGStructEntry
   * @throws WGAPIException
   */
  public WGStructEntry getStructEntry() throws WGAPIException {

    if (isDummy()) {
      return null;
    }
   
    WGStructEntry theEntry = this.db.getStructEntryByKey(getContentKey().getStructKey());
    if (theEntry != null) {
        return theEntry;
    }
    else {
        theEntry = db.getOrCreateStructEntryObject(new WGFakeStructEntry(db, getStructKey()));
        theEntry.setDummy(true);
        return theEntry;
    }
   
  }

  /**
   * Returns the unique key of this document, which can be used instead of the content key in an URL.
   * @return String
   * @throws WGAPIException
   */
  public String getUniqueName() throws WGAPIException {
    return (String) this.getMetaData(META_UNIQUE_NAME);
  }

  /**
   * Returns the workflow status of this content document, which is one of the constants WGContent.STATUS_...
   * @return String
   * @throws WGAPIException
   */
  public String getStatus() throws WGAPIException {
    return this.getMetaData(META_STATUS).toString();
  }

  /**
   * Makes a complete test about the visibility of the content in publishing:
   * - Is the general visibility flag set?
   * - Is the content visible for the given role?
   * - Are the content validity dates allowing publishing right now?
   * @param displayType The navigation element to test, if this content is shown in it. Should be one of the constants WGContent.DISPLAYTYPE_...
   * @return boolean
   * @throws WGAPIException
   */
  public boolean isVisibleFor(String displayType) throws WGAPIException {

    // Static visibility information is cached
    Boolean isVisibleFor = (Boolean) this.isVisibleFor.get(displayType);
    if (!isCachingEnabled() || isVisibleFor == null) {
      if (this.isVisible() == false) {
        isVisibleFor = new Boolean(false);
      }
      else if (displayType != null && this.isHiddenFrom().contains(displayType)) {
        isVisibleFor = new Boolean(false);
      }
      else {
        isVisibleFor = new Boolean(true);
      }
           
            if (getDatabase().getSessionContext().isCacheWritingEnabled()) {
                this.isVisibleFor.put(displayType, isVisibleFor);
            }
    }
   
    if (isVisibleFor.booleanValue() == false) {
      return false;
    }

    // Dynamic visibility information is evaluated everytime
    Date now = new Date();
    if (this.getValidFrom() != null && this.getValidFrom().after(now)) {
      return false;
    }
    else if (this.getValidTo() != null && this.getValidTo().before(now)) {
      return false;
    }
   
    return true;

  }

  /**
   * Tests the general visibility of this content (see WGContent.isVisible()), and if the current settings for the "Valid from" and "Valid to" date
   * allow it to the visible right now.
   * @return boolean
   * @throws WGAPIException
   */
  public boolean isVisibleNow() throws WGAPIException {

    if (this.isVisible() == false) {
      return false;
    }

    Date validFrom = this.getValidFrom();
    Date validTo = this.getValidTo();
    Date now = new Date();

    if (validFrom != null && validFrom.after(now)) {
      return false;
    }
    else if (validTo != null && validTo.before(now)) {
      return false;
    }

    return true;
  }

  /**
   * Returns the general visibility setting of this document. If this method returns true, the document can nevertheless be not visible bc. of other settings.
   * If this method returns false, the document is in no case visible.
   * @return boolean
   * @throws WGAPIException
   */
  public boolean isVisible() throws WGAPIException {
    return ((Boolean)this.getMetaData(META_VISIBLE)).booleanValue();
  }

  /**
   * Returns the Date, since when this document is valid (i.e. is shown in the website) or null if no such Date is specified.
   * @return Date
   * @throws WGAPIException
   */
  public java.util.Date getValidFrom() throws WGAPIException {
    return (java.util.Date) this.getMetaData(META_VALID_FROM);
  }

  /**
   * Returns the Date, until when this document is valid (i.e. is shown in the website) or null if no such Date is specified.
   * @return Date
   * @throws WGAPIException
   */
  public java.util.Date getValidTo() throws WGAPIException {
    return (java.util.Date) this.getMetaData(META_VALID_TO);
  }

  /**
   * Returns a list of the navigation elements, where this content document should not be shown.
   * These are represented as constants WGContent.DISPLAYTYPE_...
   * @return List
   * @throws WGAPIException
   */
  public java.util.List isHiddenFrom() throws WGAPIException {
    return (List) this.getMetaData(META_IS_HIDDEN_FROM);
  }

  /**
   * Returns a link url, that is called instead of an URL to this content, when it is clicked in a navigator.
   * If this field is filled, the content document is regarded a "virtual document" bc. it's content is never shown
   * and it is only used as a pointer to the link url.
   * @return The virtual link url.
   * @throws WGAPIException
   */
  public String getVirtualLink() throws WGAPIException {
    return (String) this.getMetaData(META_VIRTUAL_LINK);
  }

  /**
   * Returns the type of this virtual link, which is one of the constants WGContent.VIRTUALLINKTYPE_...
   * The type of a virtual link decides, if the link retrieved via WGContent.getVirtualLink() is a relative url,
   * and how it is completed to an absolute url at runtime.
   * @return String
   * @throws WGAPIException
   */
  public String getVirtualLinkType() throws WGAPIException {
    return (String) this.getMetaData(META_VIRTUAL_LINK_TYPE);
  }

  /**
   * Determines, if this document is a "virtual document", i.e. if another link is shown instead of a link to this content in navigators.
   * @return boolean
   * @throws WGAPIException
   */
  public boolean isVirtual() throws WGAPIException {
        String link = getVirtualLink();
    if (link == null || link.equals("")) {
      return false;
    }
    else {
      return true;
    }
  }

  /**
   * Returns the link target of this document, i.e. the name of the frame/browser window, in which this content is displayed
   * when it is clicked in a navigator.
   * @return String
   * @throws WGAPIException
   */
  public String getLinkTarget() throws WGAPIException {
    return (String) this.getMetaData(META_LINK_TARGET);
  }

  /**
   * Returns the struct key for this content document, which is the key of the struct entry this content document is attached to.
   * @return Object
   * @throws WGAPIException
   */
  public Object getStructKey() throws WGAPIException {
    return this.getMetaData(META_STRUCTENTRY);
  }

  /**
   * Returns the content key of this document, either in unique mode (real version is always used) or in URL mode (0 is used as version, when document is released)
   * @param unique Determines, if the unique mode is used. If false, the URL mode is used.
   * @return WGContentKey
   * @throws WGAPIException
   */
  public WGContentKey getContentKey(boolean unique) throws WGAPIException {

    if (unique) {
      return retrievalContentKey;
    }
    else {
      return WGContentKey.create(this, false);
    }
  }

  /**
   * Returns the unique content key of this content document.
   * @return WGContentKey
   * @throws WGAPIException
   */
  public WGContentKey getContentKey() throws WGAPIException {
    return getContentKey(true);
  }

  /**
   * Returns the version number of this content document.
   * All content documents under one struct entry in the same language share a version number pool beginning with 1.
   * @return int
   * @throws WGAPIException
   */
  public int getVersion() throws WGAPIException {
    return ((Integer)this.getMetaData(WGContent.META_VERSION)).intValue();
  }
  /* (non-Javadoc)
   * @see de.innovationgate.webgate.api.WGDocument#retrieveCore()
   */
  public WGDocumentCore retrieveCore() throws WGAPIException {
    return this.db.getCore().getContentByKey(this.retrievalContentKey);
  }

  /**
   * Sets the title of this content document.
   * @param title
   * @throws WGAPIException
   */
  public void setTitle(String title) throws WGAPIException {
    this.setMetaData(WGContent.META_TITLE, title);
  }
 
  /**
     * Sets the owner of the content document
     * @param owner
     * @throws WGAPIException
     */
    public void setOwner(String owner) throws WGAPIException {
        this.setMetaData(WGContent.META_OWNER, owner);
    }
  /**
   * Returns a list of keywords for this content document to be used to identify this document in search machines.
   * @return List
   * @throws WGAPIException
   */
  public List getKeywords() throws WGAPIException {
    return (List) this.getMetaData(META_KEYWORDS);
  }


  /**
   * Sets the workflow status of this content document.
   * @param status
   * @throws WGAPIException
   */
  protected void setStatus(String status) throws WGAPIException {
    this.setMetaData(WGContent.META_STATUS, status);
  }

  /**
   * Sets the unique name of this content document, which can be used in URLs instead of the content key.
   * @param name
   * @throws WGAPIException
   */
  public void setUniqueName(String name) throws WGAPIException {
    this.setMetaData(WGContent.META_UNIQUE_NAME, name);
  }

  /**
   * Sets the navigation elements, in that this content should not be shown.
   * @param hiddenFrom List of navigation elements, represented as constants WGContent.DISPLAYTYPE_..., where to hide this content.
   * Provide empty list or null completely disable hiding.
   * @throws WGAPIException
   */
  public void setHiddenFrom(List hiddenFrom) throws WGAPIException {

    if (hiddenFrom == null) {
      hiddenFrom = new ArrayList();
    }

    this.setMetaData(WGContent.META_IS_HIDDEN_FROM, hiddenFrom);
  }

  /**
   * Sets the general visibility flag, choosing if this document is visible. If set to "true" document can be nevertheless invisible because of other settings.
   * @param visible
   * @throws WGAPIException
   */
  public void setVisible(boolean visible) throws WGAPIException {
    this.setMetaData(WGContent.META_VISIBLE, new Boolean(visible));
  }

  /**
   * Sets validity dates, narrowing the period of time, in which a document will be visible.
   * Provide null as parameter if you do not wish to limit the period in one or both directions.
   * @param from Date, from which the content will be visible (inclusive)
   * @param to Date, to which the content will be visible (inclusive)
   * @throws WGAPIException
   */
  public void setValidity(Date from, Date to) throws WGAPIException {
    this.setMetaData(WGContent.META_VALID_FROM, from);
    this.setMetaData(WGContent.META_VALID_TO, to);
  }





  /**
   * Sets a virtual link for this document, making the content document a "virtual document".
   * The link will be shown in navigators instead of a link to this content document.
   * @param type Type of the virtual link. Deciding how "explicit" the link url needs to be. Represented by the following constants at object WGContent:
   * </p>VIRTUALLINKTYPE_CONTENT: Link to another content document. Only content key is needed as URL<br/>
   * VIRTUALLINKTYPE_EXTERNAL: External link. Absolute link needed<br/>
   * VIRTUALLINKTYPE_FILE: Link to file resource. Relative link to the file resource needed.<br/>
   * VIRTUALLINKTYPE_INTERNAL_FILE: Link to file, attached to this content document. Only file name needed.</br>
   * </p>
   * @param url The url given for the virtual link.
   * @throws WGAPIException
   */
  public void setVirtualLink(String type, String url) throws WGAPIException {
    this.setMetaData(WGContent.META_VIRTUAL_LINK_TYPE, type);
    this.setMetaData(WGContent.META_VIRTUAL_LINK, url);
  }

  /**
   * Disables virtual link functionality for this content document and clears all fields related to it
   * @throws WGAPIException
   */
  public void clearVirtualLink() throws WGAPIException {
    this.setMetaData(WGContent.META_VIRTUAL_LINK_TYPE, null);
    this.setMetaData(WGContent.META_VIRTUAL_LINK, null);
  }

  /**
   * Sets the link target of this content, i.e. the frame or browser window name, in which this content should be displayed
   * when clicked in a navigator.
   * @param target
   * @throws WGAPIException
   */
  public void setLinkTarget(String target) throws WGAPIException {
    this.setMetaData(WGContent.META_LINK_TARGET, target);
  }

  /**
   * Sets keywords for this content to provide for internet search services.
   * @param keywords List of String keywords
   * @throws WGAPIException
   */
  public void setKeywords(List keywords) throws WGAPIException {
    this.setMetaData(WGContent.META_KEYWORDS, keywords);
  }

  /**
   * Returns the name of the content author.
   * @return String
   * @throws WGAPIException
   */
  public String getAuthor() throws WGAPIException {
    return (String) this.getMetaData(WGContent.META_AUTHOR);
  }
 
  /**
     * Returns the name of the content owner.
     * @return String
     * @throws WGAPIException
     */
    public String getOwner() throws WGAPIException {
        return (String) this.getMetaData(WGContent.META_OWNER);
    }

  /**
   * If the document was retrieved via query (and the db implementation supports this feature)
   * returns the search score of this document, i.e. a number representing how much this document matches
   * the search query.
   * @return int
   * @throws WGAPIException
   */
  public float getSearchScore() throws WGAPIException {
     
      // In case of lucene search: Try to retrieve score that was inserted from outside
      Float score = getSessionData().getSearchScore();
      if (score != null) {
          return score.floatValue();
      }
     
      // In case of native search (domino fulltext): Try to retrieve score from backend document
    return ((Integer)this.getMetaData(WGContent.META_SEARCHSCORE)).floatValue();
  }
 
  /**
   * Returns an explaination object that shows information about the choosage of this content as a query result in the most recent query.
   * The type of object returned here is dependent on the type of query.
   */
  public Object getSearchExplanation() {
    return getSessionData().getSearchExplaination();   
  }

  /**
   * Releases this document immediately while exiting workflow. The user must be a valid admin approver of the current workflow to do this action.
   * @param comment Comment for the release action.
   * @throws WGAPIException
   */
  public void releaseImmediately(String comment) throws WGAPIException {

    // Test access level
    int accessLevel = this.db.getSessionContext().getAccessLevel();
    String user = this.db.getSessionContext().getUser();
    if (accessLevel < WGDatabase.ACCESSLEVEL_AUTHOR
      || (accessLevel == WGDatabase.ACCESSLEVEL_AUTHOR && !isAuthorOrOwner())) {
      throw new WGAuthorisationException("You are not authorized to publish this document");
    }

    // Test doc status
    /*
    if (!this.getStatus().equals(WGContent.STATUS_DRAFT)) {
      throw new WGWorkflowException("The content is not in status draft and cannot be published");
    }
    */
    // Test workflow role
    WGWorkflow workflow = getWorkflow();
    if (workflow.getWorkflowRole() != WGWorkflow.ROLE_ADMINISTRATOR) {
      throw new WGAuthorisationException("You are no workflow administrator");
    }

    synchronized( db ) {
   
      // release
      release(comment, workflow);
 
      // Write workflow history
      this.addWorkflowHistoryEntry("Released immediately by workflow admin");
     
      WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_STATUSCHANGED, getDocumentKey(), getStructEntry().getContentType().getName(), getDatabase());
            event.setContent(this);
            getDatabase().fireContentEvent(event);
 
      this.save();
    }

  }
 
  /**
   * For a content document in status DRAFT, set it to status REVIEW and let it enter publishing workflow.
   * @param comment A comment for publishing this content.
   * @throws WGAPIException
   */
  public void publish(String comment) throws WGAPIException {
    publish(comment, null);
  }
 
     /**
     * For a content document in status DRAFT, set it to status REVIEW and let it enter publishing workflow.
     * @throws WGAPIException
     */
     public void publish() throws WGAPIException {
        publish("", null);
     }
 
  /**
   * For a content document in status DRAFT, set it to status REVIEW and let it enter publishing workflow.
   * @param comment A comment for publishing this content.
   * @param reasonForReplacement If a released content allready exists, a reason for it's replacement can be set by this param. Allowes null value.
   * @throws WGAPIException
   */
  public void publish(String comment,String reasonForReplacement) throws WGAPIException {

    // Test access level
    int accessLevel = this.db.getSessionContext().getAccessLevel();
    String user = this.db.getSessionContext().getUser();
    if (accessLevel < WGDatabase.ACCESSLEVEL_AUTHOR
      || (accessLevel == WGDatabase.ACCESSLEVEL_AUTHOR && !isAuthorOrOwner())) {
      throw new WGAuthorisationException("You are not authorized to publish this document");
    }

    if (!this.getStatus().equals(WGContent.STATUS_DRAFT)) {
      throw new WGIllegalStateException("The content is not in status draft and cannot be published");
    }
   
    // Set ITEM_REPLACEREASON (We must save after setting that to workaround B0000445E)
   
    if( reasonForReplacement != null && !reasonForReplacement.trim().equals("") ){
      this.setItemValue(ITEM_REPLACEREASON, reasonForReplacement);
      save(new Date(), false);   
    }
    else{     
      if( this.hasItem(ITEM_REPLACEREASON) ){
        this.removeItem(ITEM_REPLACEREASON);
        save(new Date(), false);
      }
    }
       
    // Content should be saved prior to publishing (Also workaround for bug B0000445E)
    if (save() == false) {
            throw new WGBackendException("Could not publish document " + getContentKey().toString() + " because it could not be saved");
        }
   
   
    synchronized ( db ) {
            WGWorkflow workflow = getWorkflow();
       
        if (getDatabase().isProjectMode()) {
            release(comment, workflow);
            this.addWorkflowHistoryEntry("Released in project mode");
        }
        else {
           
            boolean result = workflow.publish(comment);
           
            if (result == false) {
                this.setStatus(WGContent.STATUS_REVIEW);
                this.addWorkflowHistoryEntry("Submitted for approval");
               
                WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_STATUSCHANGED, getDocumentKey(), getStructEntry().getContentType().getName(), getDatabase());
                  event.setContent(this);
                  getDatabase().fireContentEvent(event);
               
                // Automatic approval - approve method will automatically release content if workflow was done
                if (getDatabase().isAutoApprove()) {
                    while (result == false && workflow.isApprovableByUser()) {
                       result = approve("Automatic approval for user '" + getDatabase().getSessionContext().getUser() + "'", workflow);
                    }
                }
            }
           
            // Release doc if publish returned true
            else {
                  this.release(comment, workflow);
                  this.addWorkflowHistoryEntry("Released");
                 
                  WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_STATUSCHANGED, getDocumentKey(), getStructEntry().getContentType().getName(), getDatabase());
                  event.setContent(this);
                  getDatabase().fireContentEvent(event);
            }
        }
       

 
      if (save(new Date(), false) == false) {
        throw new WGBackendException("Could not publish document " + getContentKey().toString() + " because it could not be saved");
      }
    }

  }

  /**
   * For a content document in status REVIEW, approves this document and continues the workflow. The user must be a valid approver of the current workflow level to do this action.
   * @param comment A comment for the approval action
   * @throws WGAPIException
   */
  public void approve(String comment) throws WGAPIException {
      WGWorkflow workflow = getWorkflow();
      approve(comment, workflow);
  }
 
  private boolean approve(String comment, WGWorkflow workflow) throws WGAPIException {

      // Content should be saved prior to approval
        if (!isSaved() || isEdited()) {
            if (save() == false) {
                throw new WGBackendException("Could not approve document " + getContentKey().toString() + " because it could not be saved");
            }
        }
       
        // Test access level
    int accessLevel = this.db.getSessionContext().getAccessLevel();
    String user = this.db.getSessionContext().getUser();
    if (accessLevel < WGDatabase.ACCESSLEVEL_AUTHOR
      || (accessLevel == WGDatabase.ACCESSLEVEL_AUTHOR && !isAuthorOrOwner())) {
      throw new WGAuthorisationException("You are not authorized to approve this document");
    }

    // Test doc status
    if (!this.getStatus().equals(WGContent.STATUS_REVIEW)) {
      throw new WGIllegalStateException("The content is not in status review and cannot be approved");
    }

    // Test workflow role
    if (workflow.getWorkflowRole() < WGWorkflow.ROLE_APPROVER) {
      throw new WGAuthorisationException("You are no approver for the current workflow level");
    }

    synchronized( db ) {
   
      // Write workflow history
        boolean result;
      if (workflow.approve(comment)) {
        release(comment, workflow);
        this.addWorkflowHistoryEntry("Approved and released");
       
            WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_STATUSCHANGED, getDocumentKey(), getStructEntry().getContentType().getName(), getDatabase());
            event.setContent(this);
            getDatabase().fireContentEvent(event);
           
            result = true;
      }
      else {
        this.addWorkflowHistoryEntry("Approved and submitted for further approval");
        result = false;
      }
 
      if (save(new Date(), false) == false) {
        throw new WGBackendException("Could not approve document " + getContentKey().toString() + " because it could not be saved");
      }
     
      return result;
   
    }

  }

  /**
   * Internal method to process a workflow release.
   * @param comment Comment for the release
   * @param workflow A workflow object for the current content document.
   * @throws WGAPIException
   */
  private void release(String comment, WGWorkflow workflow) throws WGAPIException {

    // Get previously released. If present, archive it (in project mode: delete it)
      boolean didArchive = false;
    WGContent prevContent = getStructEntry().getReleasedContent(this.getLanguage().getName());
    Date publishedDate = new Date();
        if (prevContent != null) {
            if (getDatabase().isProjectMode()) {
                prevContent.remove();
            }
            else {
          if( this.hasItem(ITEM_REPLACEREASON) ){
            prevContent.setItemValue(ITEM_REPLACEREASON, this.getItemValue(ITEM_REPLACEREASON));
                    prevContent.save(publishedDate, false);
          }
          prevContent.archive(comment, false);
            }
            didArchive = true;
    }

    if (getStructEntry().hasReleasedContent(this.getLanguage().getName())) {
          if (didArchive) {
              throw new WGIllegalStateException("Cannot release because after archiving the previous release, there still seems to be a released content.");
          }
          else {
              throw new WGIllegalStateException("Cannot release because there is a released content that is invisible to the current user " + getDatabase().getSessionContext().getUser());
          }
      }

    this.setStatus(WGContent.STATUS_RELEASE);
   
    if (getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5) {
        this.setPublished(publishedDate);
        WGStructEntry entry = getStructEntry();
        Map published = entry.getPublished();
        if (!published.containsKey(getLanguage().getName())) {
            try {
                    published.put(getLanguage().getName(), publishedDate);
                    entry.setPublished(published);
                }
                catch (Exception e) {
                    WGFactory.getLogger().error("Exception setting published date on struct entry '" + String.valueOf(entry.getStructKey()) + "'", e);
                }
        }
    }
   
    if( this.hasItem(ITEM_REPLACEREASON) ){
      this.removeItem(ITEM_REPLACEREASON);
    }
    workflow.release(comment);

  }

  /**
   * Method reject.
   * For a content document in status REVIEW, rejects this document and resets it to status DRAFT. The user must be a valid approver of the current workflow level to do this action.
   * @param comment A comment for the rejectal action
   * @throws WGAPIException
   */
  public void reject(String comment) throws WGAPIException {

        // Content should be saved prior to rejection to enforce validity
        if (!isSaved() || isEdited()) {
            if (save() == false) {
                throw new WGBackendException("Could not reject document " + getContentKey().toString() + " because it could not be saved");
            }
        }
       
    // Test access level
    int accessLevel = this.db.getSessionContext().getAccessLevel();
    String user = this.db.getSessionContext().getUser();
    if (accessLevel < WGDatabase.ACCESSLEVEL_AUTHOR
      || (accessLevel == WGDatabase.ACCESSLEVEL_AUTHOR && !isAuthorOrOwner())) {
      throw new WGAuthorisationException("You are not authorized to reject this document");
    }

    // Test document status
    if (!this.getStatus().equals(WGContent.STATUS_REVIEW) && !this.getStatus().equals(WGContent.STATUS_RELEASE)) {
      throw new WGIllegalStateException("The content is not in status review or release and cannot be rejected");
    }

    // Test workflow role
    WGWorkflow workflow = getWorkflow();
    if (workflow.getWorkflowRole() < WGWorkflow.ROLE_APPROVER) {
      throw new WGAuthorisationException("You are no approver for the current workflow level");
    }
   
    String previousStatus = getStatus();

    // Reject
    workflow.reject(comment);
    this.setStatus(WGContent.STATUS_DRAFT);
   
    // If the previous state was released, query- and structentry cache must be cleared (default cache maintenance won't do this)
    if (previousStatus == WGContent.STATUS_RELEASE) {
      getDatabase().queryCache.clear();
      getStructEntry().dropCache();
    }

    // Write history
    this.addWorkflowHistoryEntry("Rejected and returned to draft status");
   
        WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_STATUSCHANGED, getDocumentKey(), getStructEntry().getContentType().getName(), getDatabase());
        event.setContent(this);
        getDatabase().fireContentEvent(event);

    if (save(new Date(), false) == false) {
      throw new WGBackendException("Could not reject document " + getContentKey().toString() + " because it could not be saved");
    }

  }

  /**
   * For a content document in Status RELEASE archives this document immediately. User must be the author of this document or have editor rights.
   * @param comment A comment for archiving this document
   * @throws WGAPIException
   */
  public void archive(String comment) throws WGAPIException {
      archive(comment, true);
  }
 
  private void archive(String comment, boolean checkProtectedRelations) throws WGAPIException {

      // Check if a protected relation prevents archiving of this content
        if (checkProtectedRelations && getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5) {
            for (WGRelationData rel : getIncomingRelations(true)) {
                if (rel.getType() == WGContent.RELATIONTYPE_PROTECTED) {
                    throw new WGRestrictionException("The content is target of protected relation '" + rel.getName() + "' from content '" + rel.getParentContentKey().toString() + "' and cannot be deleted");
                }
            }
        }
     
        // Content should be saved prior to archiving
        if (!isSaved() || isEdited()) {
            if (save() == false) {
                throw new WGBackendException("Could not archive document " + getContentKey().toString() + " because it could not be saved");
            }
        }
       
        // Test access level
    int accessLevel = this.db.getSessionContext().getAccessLevel();
    String user = this.db.getSessionContext().getUser();
    if (accessLevel < WGDatabase.ACCESSLEVEL_AUTHOR
      || (accessLevel == WGDatabase.ACCESSLEVEL_AUTHOR && !isAuthorOrOwner())) {
      throw new WGAuthorisationException("You are not authorized to archive this document because you are not the author");
    }

    if (!this.getStatus().equals(WGContent.STATUS_RELEASE) && !this.getStatus().equals(WGContent.STATUS_DRAFT)) {
      throw new WGIllegalStateException("The content is in status '" + getStatus() + "' and cannot be archived");
    }

    // Archive
    this.setStatus(WGContent.STATUS_ARCHIVE);
    WGWorkflow workflow = getWorkflow();
    workflow.archive(comment);

    // Write workflow history
    if (comment != null && !comment.trim().equals("")) {
      this.addWorkflowHistoryEntry("Archived. Replace reason: " + comment);
    }
    else {
      this.addWorkflowHistoryEntry("Archived");
    }
   
        WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_STATUSCHANGED, getDocumentKey(), getStructEntry().getContentType().getName(), getDatabase());
        event.setContent(this);
        getDatabase().fireContentEvent(event);
   

    if (save(new Date(), false) == false) {
      throw new WGBackendException("Could not archive document " + getContentKey().toString() + " because it could not be saved");
    }

  }

  /**
   * Adds an entry to the end of the workflow history. The entry count is kept at a maximum number of 50.
   * Therefor the oldest entries will be removed when this number is exceeded.
   * @param entry The entry to add.
   * @throws WGAPIException
   */
  protected void addWorkflowHistoryEntry(String entry) throws WGAPIException {

    List workflowHistory = this.getMetaDataList(META_WFHISTORY);
    String dateFormatted = DATEFORMAT_HISTORY.format(new Date());
   
    String completeEntry = dateFormatted + " - Version " + getVersion() + " - User " + getDatabase().getSessionContext().getUser()" - " + entry;
        completeEntry = WGUtils.reduce(completeEntry, 250);
       
        workflowHistory.add(completeEntry);
    while (workflowHistory.size() > 50) {
      workflowHistory.remove(0);
    }
    this.setMetaData(META_WFHISTORY, workflowHistory);

  }

  /**
   * Saves changes made to this document. You must be a valid editor to do this.
   * @throws WGAPIException
   */
    public boolean save(Date lastModified) throws WGAPIException {
        return save(lastModified, true);
    }
   
  private boolean save(Date lastModified, boolean performTests) throws WGAPIException {

        String contentType = null;
        if (hasCompleteRelationships()) {
            contentType = getStructEntry().getContentType().getName();
        }

        // Event contentSaved (semaphore-checked to prevent recursive savings)
        if (!getSessionData().isSaveEventSemaphore()) {
            getSessionData().setSaveEventSemaphore(true);
            try {
                WGContentEvent event = new WGContentEvent(WGContentEvent.TYPE_SAVED, getDocumentKey(), contentType, getDatabase());
                event.setContent(this);
                if (db.fireContentEvent(event) == false) {
                    return false;
                }
            }
            finally {
                getSessionData().setSaveEventSemaphore(false);
            }
        }
       
    //  Set last client
    String client = getDatabase().getSessionContext().getClient();
    if (client != null) {
      setLastClient(getDatabase().getSessionContext().getClient());
    }
   
    // Set visibility
    if (hasCompleteRelationships() && getStructEntry().getArea().isSystemArea()) {
        setVisible(false);
    }
    else if (!isVisible()) {
        setVisible(true);
    }

    try {
        getSessionData().setPerformStatusTestsOnSave(performTests);
            boolean result = super.save(lastModified);
            gatherRetrievalKeys();
            return result;
    }
    finally {
        getSessionData().setPerformStatusTestsOnSave(false);
    }

  }
   
    /**
     * Tests if the current user is allowed to edit (i.e. save modifications) the current document
     * @throws WGAPIException
     */
    public boolean mayEditContent() throws WGAPIException {
       
        try {
            performSaveCheck();
            return true;
        }
        catch (WGAuthorisationException e) {
            return false;
        }
        catch (WGIllegalStateException e) {
            return false;
        }
       
    }


    public void performSaveCheck() throws WGAuthorisationException, WGAPIException {
       
        super.performSaveCheck();
       
        // Integrity checks. Must be done for all users (including master)
       
        if (isDummy()) {
            throw new WGIllegalStateException("This is a dummy content that cannot be saved");
        }
       
        // Check if Unique Name exists
        if (getStatus().equals(WGContent.STATUS_RELEASE) && db.hasFeature(WGDatabase.FEATURE_FULLCONTENTFEATURES) &&
                getDatabase().getSessionContext().isTestUniqueNames()) {
            boolean isUniqueNameOK = false;

            // B00004D4E
            if (this.getUniqueName() != null && !this.getUniqueName().equals("")) {
                UniqueNameTester tester = null;
                try {
                    tester = new UniqueNameTester(this.getUniqueName(), getLanguage().getName(), getStructKey());
                    Thread proveThread = new Thread(tester);
                    proveThread.start();
                    proveThread.join();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
                isUniqueNameOK = tester.isUniqueNameOK;
            }
            else {
                isUniqueNameOK = true;
            }

            if (!isUniqueNameOK) {
                throw new WGDuplicateKeyException("Unique name " + this.getUniqueName() + " already exists.");
            }
        }
       
        // Master sessions may bypass all save checks that are for authorisation only
        if (getDatabase().getSessionContext().isMasterSession()) {
            return;
        }
       
        // Accesslevel EDITOR is needed everywhere where released docs are saved
        boolean performStatusTests = getSessionData().isPerformStatusTestsOnSave();
        int accessLevel = db.getSessionContext().getAccessLevel();
       
        if (performStatusTests && getStatus().equals(WGContent.STATUS_RELEASE)) {
            if (accessLevel < WGDatabase.ACCESSLEVEL_EDITOR) {
                throw new WGAuthorisationException("This content is released and can only be edited by editors or higher");
            }
        }

        // Checks only done on full content stores
        if (db.hasFeature(WGDatabase.FEATURE_FULLCONTENTFEATURES)) {
       
            // Check (hierarchical) editor rights from struct
            WGDocument document = this.getStructEntry().mayEditEntryAndContent();
            if (document != null) {
              if (document instanceof WGArea) {
                throw new WGAuthorisationException("User is not allowed to edit content in this area");
              }
              else {
                WGStructEntry entry = (WGStructEntry) document;
                throw new WGAuthorisationException(
                  "User is not allowed to edit this content, because struct entry '"
                    + entry.getTitle()
                    + "' (Key "
                    + entry.getStructKey()
                    + ") disallows it");
              }
            }
   
            // Check edit rights from content type
            WGContentType contentType = getStructEntry().getContentType();
            if (contentType != null && !contentType.mayCreateContent()) {
              throw new WGAuthorisationException("User is not allowed to edit content of this content type");
            }
   
            // Check edit rights from language
            WGLanguage language = getLanguage();
            if (language != null && !language.mayCreateContent()) {
              throw new WGAuthorisationException("User is not allowed to edit content of this language");
            }
   
            // If this is a content without content type it can only be edited by designers
            else if (contentType == null && getDatabase().getSessionContext().isDesigner() == false) {
              throw new WGAuthorisationException("User is not allowed to edit content without content type");
            }
   
            // Check author status
            if (accessLevel == WGDatabase.ACCESSLEVEL_AUTHOR) {
              if (!isAuthorOrOwner()) {
                throw new WGAuthorisationException("You are not authorized to save this document, because you are no author");
              }
            }
           
            // Check workflow status and special dependencies related to them
            if (performStatusTests) {
                if (getStatus().equals(WGContent.STATUS_REVIEW)) {
                  WGWorkflow workflow = db.getWorkflowEngine().getWorkflow(this);
                if (workflow.getWorkflowRole() < WGWorkflow.ROLE_APPROVER) {
                  throw new WGAuthorisationException("This content is in review and can only be edited by an appover or workflow administrator");
                }
                }
       
                if (getStatus().equals(WGContent.STATUS_ARCHIVE)) {
                  if (accessLevel < WGDatabase.ACCESSLEVEL_MANAGER) {
                    throw new WGAuthorisationException("This content is archived and can only be edited by a manager");
                  }
                }
            }
        }
       
    }
   
    /**
     * Determines if the current user is an author of this document
     */
    public boolean isAuthorOrOwner() throws WGAPIException {
       
        List userNameList = new ArrayList();
        userNameList.add(getAuthor());
        userNameList.add(getOwner());
        userNameList.addAll(getCoauthors());
        return db.isMemberOfUserList(userNameList);
       
    }



  /**
   * Returns a description for this content document
   * @return String
   * @throws WGAPIException
   */
  public String getDescription() throws WGAPIException {
    return (String) this.getMetaData(META_DESCRIPTION);
  }

  /**
   * Set a description for this content document.
   * @param desc
   * @throws WGAPIException
   */
  public void setDescription(String desc) throws WGAPIException {
    this.setMetaData(META_DESCRIPTION, desc);
  }

  /**
   * Returns the role of the current user in the content's current workflow state.
   * @return A constant of type WGWorkflow.ROLE_....
   * @throws WGAPIException
   */
  public int getWorkflowRole() throws WGAPIException {

    WGWorkflow workflow = getWorkflow();
    return workflow.getWorkflowRole();
  }

    /**
     * Retrieves the workflow object for the current content document, if it is available
     * @throws WGAPIException
     */
    public WGWorkflow getWorkflow() throws WGAPIException {
        return this.db.getWorkflowEngine().getWorkflow(this);
    }

  /* (Kein Javadoc)
   * @see de.innovationgate.webgate.api.WGDocument#createClone(de.innovationgate.webgate.api.WGDatabase)
   */
  public WGDocument createClone(WGDatabase db, WGDocument ref) throws WGAPIException {

    if (ref == null || !(ref instanceof WGStructEntry)) {
      return null;
    }

    // Metadata
    WGContent newContent =
      db.createContent(
        (WGStructEntry) ref,
        db.getLanguage((String) getMetaData(META_LANGUAGE)),
        getTitle(),
        new Integer(getVersion()));
    newContent.save();
   
        try {
            pushData(newContent);
   
        if (newContent.save(getLastModified())) {
          return newContent;
        }
        else {
          return null;
        }
        } catch (WGAPIException e) {
            // try to remove created content object
            try {
                newContent.remove();
            } catch (WGAPIException e1) {               
            }
           
            throw e;
        }

  }

    public void pushData(WGDocument newDoc) throws WGAPIException, WGCreationException {
       
        super.pushData(newDoc);
        WGContent newContent = (WGContent) newDoc;
        newContent.setMetaData(META_AUTHOR, getAuthor());
       
        // New WGACS 5 Metas
        if (getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5 && newDoc.getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5) {
            newContent.setOwner(getOwner());
            newContent.setCoauthors(new ArrayList(getCoauthors()));
            newContent.setContentClass(getContentClass());
            newContent.setPublished(getPublished());
        }
       
        newContent.setTitle(getTitle());
        newContent.setDescription(getDescription());
        newContent.setHiddenFrom(new ArrayList(getMetaDataList(META_IS_HIDDEN_FROM)));
        newContent.setKeywords(new ArrayList(getKeywords()));
        newContent.setLinkTarget(getLinkTarget());
  
        newContent.setStatus(getStatus());
       
        newContent.setUniqueName(getUniqueName());
        newContent.setValidity(getValidFrom(), getValidTo());
       
        if (isVirtual()) {
          newContent.setVirtualLink(getVirtualLinkType(), getVirtualLink());
        }
        else {
            newContent.clearVirtualLink();
        }
       
        newContent.setVisible(isVisible());
        newContent.setReaders(new ArrayList(getReaders()));
        newContent.setLastClient(getLastClient());
       
       
        // Clear all items and files
        if (newContent.getItemNames().size() > 0) {
            newContent.removeAllItems();
            newContent.save();
        }
       
        if (newContent.getFileNames().size() > 0) {
            newContent.removeAllFiles();
            newContent.save();
        }
       
        if (newDoc.getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5 && newContent.getRelationNames().size() > 0) {
            newContent.removeAllRelations();
            newContent.save();
        }
       
        // Copy Items
        Iterator itemNames = getItemNames().iterator();
        String itemName;
        while (itemNames.hasNext()) {
          itemName = (String) itemNames.next();
          try {
                newContent.setItemValue(itemName, new ArrayList(getItemValueList(itemName)));
            }
            catch (WGIllegalDataException e) {
                WGFactory.getLogger().warn("Cannot copy item '" + itemName + "' because of unrestorable data", e);
            }
        }
  
        // Copy Files
      try {
          Iterator fileNames = getFileNames().iterator();
          while (fileNames.hasNext()) {
            String fileName = (String) fileNames.next();
           
            // filenames might contain path separators for e.g. in domino rtf fields (body/file.txt)
            // these files cannot be extracted without filename convertion bc. the file cannot be extracted
            // to the filesystem - therefore '/' is replaced by '�'
            // the attachFile method of the JDBC-ContentStore do the back convertion so
            // the file name stays constant after migration
            String convertedFileName = fileName.replace('/', '�');
                                   
            TemporaryFile tempFile = new TemporaryFile(convertedFileName, getFileData(fileName), WGFactory.getTempDir());
            tempFile.deleteOnEviction(getDatabase().getSessionContext());
            newContent.attachFile(tempFile.getFile());
          }
        }
      catch (IOException e) {
            WGFactory.getLogger().error("Error pushing content data", e);
            throw new WGCreationException("IO Error copying files for content clone", e);
        }
     
      // Copy relations
      if (getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5 && newDoc.getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5) {
          Iterator relations = getRelationNames().iterator();
          String relName;
          while (relations.hasNext()) {
                relName = (String) relations.next();
                WGRelationData address = getCore().getRelationData(relName);
                if (address != null) {
                    newContent.setRelation(address);
                }
                else {
                    WGFactory.getLogger().error("Cannot copy relation '" + relName + "' because it is not retrievable");
                }
            }
      }
    }
 
  /**
   * Removes all relations from this content
   * @throws WGAPIException
   */
  public void removeAllRelations() throws WGAPIException {
      Iterator names = getRelationNames().iterator();
        while (names.hasNext()) {
            String name = (String) names.next();
            removeRelation(name);
        }
    }

    /**
   * Creates a draft copy for this content that gets a new version number and starts in status "draft".
   * @return The draft copy
   * @throws WGAPIException
   */
  public WGContent createDraftCopy() throws WGAPIException {
    return getDatabase().createDraftCopy(this);
  }

  /**
   * Returns the list of allowed readers of this content.
   * @throws WGAPIException
   */
  public List getReaders() throws WGAPIException {
    return (List)getMetaDataList(META_READERS);
  }
 
  /**
     * Returns the list of co-authors of this content.
     * @throws WGAPIException
     */
    public List getCoauthors() throws WGAPIException {
        return (List)getMetaDataList(META_COAUTHORS);
    }

  /**
   * Sets a list of readers allowed to see this content
   * @param readers
   * @throws WGAPIException
   */
  public boolean setReaders(List readers) throws WGAPIException {
    return setMetaData(META_READERS, readers);
  }
 
     /**
     * Sets a list of co-authors allowed to edit this content
     * @param coauthors
     * @throws WGAPIException
     */
    public boolean setCoauthors(List coauthors) throws WGAPIException {
        return setMetaData(META_COAUTHORS, coauthors);
    }

  /**
   * Indicates if this content has "complete relationships", meaning it is no dummy, it has an associated language definition, a struct entry, and via this entry a content type
   * @throws WGAPIException
   */
  public boolean hasCompleteRelationships() throws WGAPIException {

    if (isDummy()) {
      return false;
    }

    WGLanguage lang = getLanguage();
       
        // Dummy/temporary languages are ok
    if (lang == null) {
      return false;
    }
       
        // Dummy/temporary structs and content types are not
    WGStructEntry entry = getStructEntry();
    if (entry == null || entry.isTemporary() || entry.isDummy()) {
      return false;
    }

    WGContentType contentType = entry.getContentType();
    if (contentType == null || contentType.isTemporary() || contentType.isDummy()) {
      return false;
    }

    return true;
  }

  /* (Kein Javadoc)
   * @see de.innovationgate.webgate.api.WGDocument#remove()
   */
  public boolean remove() throws WGAPIException {

    if (!db.isSessionOpen()) {
        throw new WGClosedSessionException();
    }
    return innerRemove();
       
  }

    /* (non-Javadoc)
     * @see de.innovationgate.webgate.api.WGDocument#performRemoveCheck()
     */
    public void performRemoveCheck() throws WGAuthorisationException, WGAPIException {
       
        super.performRemoveCheck();
       
        if (db.getSessionContext().getAccessLevel() < WGDatabase.ACCESSLEVEL_EDITOR) {
           
            // In project mode all authors are allowed to delete content documents, no matter the ACL
            if (!(getDatabase().isProjectMode() && this instanceof WGContent && db.getSessionContext().getAccessLevel() >= WGDatabase.ACCESSLEVEL_AUTHOR)) {
                throw new WGAuthorisationException("You are not authorized to delete contents in this database!");
            }
    }
     
      if (db.hasFeature(WGDatabase.FEATURE_FULLCONTENTFEATURES)) {

      WGDocument document = this.getStructEntry().mayEditEntryAndContent();
      if (document != null) {
        if (document instanceof WGArea) {
          throw new WGAuthorisationException("User is not allowed to delete content in this area");
        }
        else {
          WGStructEntry entry = (WGStructEntry) document;
          throw new WGAuthorisationException(
            "User is not allowed to delete this content, because struct entry '"
              + entry.getTitle()
              + "' (Key "
              + entry.getStructKey()
              + ") disallows it");
        }

      }

      WGContentType contentType = this.getStructEntry().getContentType();
      if (contentType != null && !contentType.mayCreateContent()) {
        throw new WGAuthorisationException("User is not allowed to delete content of this content type");
      }
      else if (contentType == null && getDatabase().getSessionContext().isDesigner() == false) {
        throw new WGAuthorisationException("User is not allowed to delete content without doctype");
      }
     
      // Check restricted relations
      if (getDatabase().getContentStoreVersion() >= WGDatabase.CSVERSION_WGA5 && getDatabase().getSessionContext().isProtectedRelationsEnabled()) {
          for (WGRelationData rel : getIncomingRelations(true)) {
              if (rel.getType() == WGContent.RELATIONTYPE_PROTECTED && !getContentKey().equals(rel.getParentContentKey())) {
                  throw new WGRestrictionException("The content is target of protected relation '" + rel.getName() + "' from content '" + rel.getParentContentKey().toString() + "' and cannot be deleted");
              }
          }
      }
    }
    }
 
  /**
   * Returns the type of the last client that edited this content.
   * @return a client type string.
   * @throws WGAPIException
   */
  public String getLastClient() throws WGAPIException {
    return (String) getMetaData(META_LASTCLIENT);
  }
 
  /**
   * Sets the last client that edited this content
   * @param client The client string.
   * @throws WGAPIException
   */
  public boolean setLastClient(String client) throws WGAPIException {
    return setMetaData(META_LASTCLIENT, client);
  }
   
 
  /**
   * Determines, if a content may be published (visible to a user) right now, testing all necessary conditions.
   * @param content The content to test
   * @param isAuthoringMode When true, the WGAPI assumes usage by an author in some kind of authoring app (who may see any docs that are visibility protected), when false assumes usage by a web user that may see only released content
   * @param displayType The display type for which the content is used. Use constants WGContent.DISPLAYTYPE_...
   * @return true, if the document may be published, false if not
   * @throws WGAPIException
   */
  public static boolean mayBePublished(WGContent content, boolean isAuthoringMode, String displayType) throws WGAPIException {
     
      // Sanity
      if (content == null) {
            return false;
        }

      // When the content is in review and the user is approver for it we automatically enable authoring mode
      if (content.getStatus().equals(WGContent.STATUS_REVIEW) && content.getWorkflowRole() >= WGWorkflow.ROLE_APPROVER) {
          isAuthoringMode = true;
      }
     
      // A user below ACL level author cannot use authoring mode
      if (isAuthoringMode == true && content.getDatabase().getSessionContext().getAccessLevel() < WGDatabase.ACCESSLEVEL_AUTHOR) {
          isAuthoringMode = false;
      }
     
      // Tests that are bypassed by authoring mode
      if (!isAuthoringMode) {
         
              // Workflow status: document must be released
              if (!content.getStatus().equals(WGContent.STATUS_RELEASE)) {
                  return false;
              }

              // Visible flag
              if (content.isVisible() == false) {
                  return false;
              }

              // Valid/From to dates
              Date now = new Date();
              if (content.getValidFrom() != null && content.getValidFrom().after(now)) {
                  return false;
              }
              if (content.getValidTo() != null && content.getValidTo().before(now)) {
                  return false;
              }
         
      }
     
      // Hidden flags for navigational structures
        if (displayType != null && content.isHiddenFrom().contains(displayType)) {
            return false;
        }
       
        return true;

  }
 
  /**
   * A executed the static method "mayBePublished" on this content.
   * @param isAuthoringMode When true, the WGAPI assumes usage by an author (who can see docs with status != published), when false assumes usage by a web user that may see only released content
   * @param displayType The display type for which the content is used. Use constants WGContent.DISPLAYTYPE_...
   * @return true, if the document may be published, false if not
   * @throws WGAPIException
   */
  public boolean mayBePublished(boolean isAuthoringMode, String displayType) throws WGAPIException {
    return mayBePublished(this, isAuthoringMode, displayType);
  }

    /**
     * Sets the search score for this document on the last fulltext search.
     * This method should only get used internally by the WGAPI.
     * @param score
     */
    public void setSearchScore(float score) {
        getSessionData().setSearchScore(new Float(score));
    }
   
    /**
     * Sets the search explanation for this document
     * This method is for e.g. used by lucene in 'explain' mode to give back a detail information of the scoring
     * @param explaination
     */
    public void setSearchExplanation(Object explaination) {
      getSessionData().setSearchExplaination(explaination);
    }



    /*
     *  (non-Javadoc)
     * @see de.innovationgate.webgate.api.locking.Lockable#getParentLockable()
     */
    public Lockable getParentLockable() throws WGAPIException {
        return getStructEntry();
    }
   
    /**
     * Returns the next released content of identical language up the struct hierarchy
     * Depending on parameter skipUnreleasedParents it only tests the immediate parent or goes further up it that one has no released content in the right language.
     * It returns null if no suitable parent could be found.
     * @param skipUnreleasedParents Determines the behaviour when a parent has no released content. If true the parent is skipped and the method continues with the next higher parent. If false the method returns null.
     * @throws WGAPIException
     * @return The content or null if none was found or we are at root.
     */
    public WGContent getParentContent(boolean skipUnreleasedParents) throws WGAPIException {
       
        WGStructEntry parent = getStructEntry().getParentEntry();
        while (parent != null) {
            WGContent content = parent.getReleasedContent(getLanguage().getName());
            if (content != null) {
                return content;
            }
            if (skipUnreleasedParents) {
                parent = parent.getParentEntry();
            }
            else {
                return null;
            }
        }
       
        return null;
       
    }
   
    /**
     * Returns the next released content of identical language up the struct hierarchy
     * This variant skips parents which have no released content in the right language and returns the first parent which has one.
     * @throws WGAPIException
     * @return The content or null if none was found or we are at root.
     */
    public WGContent getParentContent() throws WGAPIException {
        return getParentContent(true);
    }
   
    /**
     * Collects all released contents of identical language on child struct entries.
     * @throws WGAPIException
     */
    public List<WGContent> getChildContents() throws WGAPIException {
       
        List<WGContent> contents  = new ArrayList<WGContent>();
        Iterator<WGStructEntry> childEntries = getStructEntry().getChildEntries().iterator();
        WGStructEntry child;
        while (childEntries.hasNext()) {
            child = childEntries.next();
            WGContent content = child.getReleasedContent(getLanguage().getName());
            if (content != null) {
                contents.add(content);
            }
        }
        return contents;
       
    }

    /**
     * Collects all released contents of identical language on sibling struct entries (including the current one)
     * @throws WGAPIException
     */
    public List getSiblingContents() throws WGAPIException {
       
        List contents  = new ArrayList();
        Iterator childEntries = getStructEntry().getSiblingEntries().iterator();
        WGStructEntry child;
        while (childEntries.hasNext()) {
            child = (WGStructEntry) childEntries.next();
            WGContent content = child.getReleasedContent(getLanguage().getName());
            if (content != null) {
                contents.add(content);
            }
        }
        return contents;
       
    }
   
    /**
     * Creates a new child page, including struct entry and content.
     * Both documents are already saved when the method exits. The language used for the new content is the one of the current content.
     * @param contentType The content type of the page
     * @param title The title of the page that will be used for both struct entry
     * @return The created content, already saved, but still in draft state
     * @throws WGAPIException
     */
    public WGContent createChildPage(WGContentType contentType, String title) throws WGAPIException {

        WGStructEntry entry = getStructEntry().createChildEntry(contentType, title);
        entry.save();
        WGContent content = entry.createContent(getLanguage(), title);
        content.save();
        return content;
       
    }
 

    /**
     * @return Returns the retrievalLanguage.
     */
    protected String getRetrievalLanguage() {
        return retrievalLanguage;
    }

    /**
     * @return Returns the retrievalStatus.
     */
    protected String getRetrievalStatus() {
        return retrievalStatus;
    }

    /**
     * @return Returns the retrievalUniqueName.
     */
    protected String getRetrievalUniqueName() {
        return retrievalUniqueName;
    }
   
    /**
     * Returns the next released content on the previous struct entries of identical language
     * Starts with the immediately previous struct entry and goes on with the one before until
     * it finds a released content. If none is found it returns null.
     * @throws WGAPIException
     * @return The content or null if none was found.
     */
    public WGContent getPreviousContent() throws WGAPIException {
       
        WGStructEntry struct = getStructEntry().getPreviousSibling();
        while (struct != null) {
            WGContent content =struct.getReleasedContent(getLanguage().getName());
            if (content != null) {
                return content;
            }
            struct = struct.getPreviousSibling();
        }

        return null;
       
    }
   
    /**
     * Returns the next released content on the following struct entries in identical language
     * Starts with the immediately next struct entry and goes on with the one after that until
     * it finds a released content. If none is found it returns null.
     * @throws WGAPIException
     * @return The content or null if none was found.
     */
    public WGContent getNextContent() throws WGAPIException {
       
        WGStructEntry struct = getStructEntry().getNextSibling();
        while (struct != null) {
            WGContent content = struct.getReleasedContent(getLanguage().getName());
            if (content != null) {
                return content;
            }
            struct = struct.getNextSibling();
        }
       
        return null;
       
       
    }
   
    private SessionData getSessionData() {
       
        DocumentContext con = getDocumentSessionContext();
        SessionData data = (SessionData) con.getCustomData();
        if (data == null) {
            data = new SessionData();
            con.setCustomData(data);
        }
        return data;
       
    }

    /**
     * Retrieves the target content document of the given content relation
     * @param strName Name of the relation
     * @return The content behind this relation
     * @throws WGAPIException
     */
    public WGContent getRelation(String strName) throws WGAPIException {
       
        if (!getDatabase().isSessionOpen()) {
            throw new WGClosedSessionException();
        }
       
        if (getDatabase().getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            return null;
        }
       
        Object value = null;
       
        Map cache = null;
        boolean cacheable = isCachingEnabled() && !this.dontCacheTheseRelations.contains(strName);
        if (cacheable) {
            cache = db.getCache().readRelationCache(this);
            if (cache == null) {
                cache = WGUtils.createSynchronizedMap();
            }
        }
       
        String cacheKey = "relation:" + strName;
        if (cache != null) {
            Object contentKey = cache.get(cacheKey);
            if (contentKey != null && contentKey instanceof WGContentKey) {
                WGContent targetContent = getDatabase().getContentByKey((WGContentKey) contentKey);
                if (targetContent != null && targetContent.getStatus().equals(WGContent.STATUS_RELEASE)) {
                    value = targetContent;
                }
            }
            else if (contentKey instanceof NullPlaceHolder) {
                value = contentKey;
            }
        }
       
        if (value == null) {
            WGDocumentCore targetCore = getCore().getRelation(strName);
            if (targetCore != null) {
                value = getDatabase().getOrCreateContentObject(targetCore);
            }
           
            if (cache != null && getDatabase().getSessionContext().isCacheWritingEnabled()) {
                Object cacheValue = (value instanceof WGContent ? ((WGContent) value).getContentKey() : null);
                cache.put(cacheKey, nullPlaceholder(cacheValue));
                db.getCache().writeRelationCache(this, cache);
            }
        }
        else if (value instanceof NullPlaceHolder) {
            value = null;
        }
       
        return (WGContent) value;
       
    }

    /**
     * Returns the names of all relations that this content document holds
     * @return List of Strings, representing relation names
     * @throws WGAPIException
     */
    public List<String> getRelationNames() throws WGAPIException {
       
        if (getDatabase().getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            return Collections.emptyList();
        }
       
        return getCore().getRelationNames();
    }
   
    /**
     * Sets a content relation to the designated target content.
     * The target content must be of the same database as the current content
     * @param name The name of the relation
     * @param target The relation target content
     * @param type The type of relation. Use constants WGContent.RELATIONTYPE_...
     * @return The previously mapped content for this relation
     * @throws WGAPIException
     */
    public WGContent setRelation(String name, WGContent target, int type) throws WGAPIException {

        this.dontCacheTheseRelations.add(name);
       
        if (target.getDatabase() != getDatabase()) {
            throw new WGIllegalArgumentException("The target content of this relation is from another database: " + target.getDatabase().getDbReference());
        }
       
        if (!target.getStatus().equals(WGContent.STATUS_RELEASE) && type == RELATIONTYPE_PROTECTED) {
            throw new WGIllegalArgumentException("You cannot set a protected relation to a non-released content: " + target.getContentKey().toString());
        }

       
        Object structKey = target.getStructKey();
        String language = target.getLanguage().getName();

        WGContent content = setRelation(new WGRelationData(getContentKey(), name, structKey, language, type, null));
        setEdited(true);
       
        return content;
       
       
    }
   
    /**
     * Sets a normal content relation to the designated target content.
     * The target content must be of the same database as the current content
     * @param name The name of the relation
     * @param target The relation target content
     * @return The previously mapped content for this relation
     * @throws WGAPIException
     */
    public WGContent setRelation(String name, WGContent target) throws WGAPIException {
        return setRelation(name, target, RELATIONTYPE_NORMAL);
    }

    /**
     * Sets a content relation with the given data
     * @param address The data of the relation to set
     * @return The previously mapped content of this relation
     * @throws WGAPIException
     */
    public WGContent setRelation(WGRelationData address) throws WGAPIException {
        WGDocumentCore previousTargetCore = getCore().setRelation(address);
        if (previousTargetCore != null) {
            return getDatabase().getOrCreateContentObject(previousTargetCore);
        }
        else {
            return null;
        }
    }
   
    /**
     * Removes a content relation
     * @param name Name of the relation
     * @return The content document previously mapped to this relation
     * @throws WGAPIException
     */
    public WGContent removeRelation(String name) throws WGAPIException {
       
        name = name.toLowerCase();
       
        WGDocumentCore previousTargetCore = getCore().removeRelation(name);
        if (previousTargetCore != null) {
            return getDatabase().getOrCreateContentObject(previousTargetCore);
        }
        else {
            return null;
        }
       
    }
   
    /**
     * Sets a virtual item for this content document.
     * Virtual items remain at the document until the current session ends.
     * They are only visible in the current session and are retrievable via
     * normal item retrieval methods like {@link #getItemValue(String)}.
     * A virtual item whose name matches a persistent item on the same document
     * hides this item untilthe end of the session.
     * @param name Name of the item
     * @param value Value of the item
     */
    public void setVirtualItemValue(String name, Object value) {
        getSessionData().getVirtualItems().put(name.toLowerCase(), value);
    }
   
    /**
     * Removes an virtual item from this content document
     * @param name
     * @param value
     * @return The value of the removed item
     */
    public Object removeVirtualItemValue(String name, Object value) {
        return getSessionData().getVirtualItems().remove(name.toLowerCase());
    }

    public List getItemNames() throws WGAPIException {
        List names = super.getItemNames();
        Map virtualItems = getSessionData().getVirtualItems();
        if (virtualItems.size() > 0) {
            Set virtualItemNames = new HashSet(virtualItems.keySet());
            virtualItemNames.removeAll(names);
            names.addAll(virtualItemNames);
        }
        return names;
    }

    public Object getItemValue(String strName) throws WGAPIException {
       
        Object value = getSessionData().getVirtualItems().get(strName.toLowerCase());
        if (value != null) {
            return cloneMutableObjects(value);
        }
       
       
        return super.getItemValue(strName);
    }

    public boolean hasItem(String itemName) throws WGAPIException {

        if (getSessionData().getVirtualItems().containsKey(itemName.toLowerCase())) {
            return true;
        }
       
        return super.hasItem(itemName);
    }
   
    /**
     * Returns the content class of this content.
     * @throws WGAPIException
     */
    public String getContentClass() throws WGAPIException {
        return (String) getMetaData(META_CONTENTCLASS);
    }
   
    /**
     * Sets the content class for this content.
     * The content class is a categorisation string for the current document
     * that can be use arbitrary. It is of special meaning when using
     * relations inversely to filter out those relations that come from a
     * special document class.
     * @param clazz The content class to set
     * @throws WGAPIException
     */
    public void setContentClass(String clazz) throws WGAPIException {
        setMetaData(META_CONTENTCLASS, clazz);
    }
   
    /**
     * Retrieves the released content of identical language on the root entry of the current content's struct
     * @throws WGAPIException
     */
    public WGContent getRootContent() throws WGAPIException {
       
        WGStructEntry root = getStructEntry().getRootEntry();
        return root.getReleasedContent(getLanguage().getName());
       
    }

    public List<PageHierarchyNode> getChildNodes() throws WGAPIException {
        return null;
    }

    public Class getChildNodeType() {
         return null;
    }

    public String getNodeKey() throws WGAPIException {
         return getDocumentKey();
    }

    public String getNodeTitle(String language) throws WGAPIException {
        return getTitle();
    }

    public PageHierarchyNode getParentNode() throws WGAPIException {
        return getStructEntry();
    }
   
    /**
     * Returns the time when this content version was published
     * @throws WGAPIException
     */
    public Date getPublished() throws WGAPIException {
        return (Date) getMetaData(META_PUBLISHED);
    }
   
    protected void setPublished(Date published) throws WGAPIException {
        setMetaData(META_PUBLISHED, published);
    }
   
    /**
     * Returns the E-Mail address of the author of the current document
     * This method remains bc. of compatibility reasons. Unlike in earlier WGA versions it does not read an E-Mail address stored in the document
     * but rather does a lookup of the address at the databases authentication module, using the currently stored name under {@link #getAuthor()}.
     * @throws WGAPIException
     */
    public String getAuthorEMail() throws WGAPIException {
       
        Map persistentStore = getPersistentStore();
        String eMail = (String) persistentStore.get(STORE_AUTHOR_EMAIL);
        if (eMail != null) {
            return eMail;
        }
       
        AuthenticationModule auth = getDatabase().getAuthenticationModule();
        if (auth == null) {
            return null;
        }
       
        eMail = auth.getEMailAddress(getAuthor());
        persistentStore.put(STORE_AUTHOR_EMAIL, eMail);
        return eMail;
       
    }
   
    /**
     * Returns a list of relations that point the current document
     * @param includeUnreleased Specify true to also retrieve documents in draft or approval state. False will retrieve only published documents.
     * @throws WGAPIException
     */
    public List<WGRelationData> getIncomingRelations(boolean includeUnreleased) throws WGAPIException {
       
        if (getDatabase().getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            return Collections.emptyList();
        }
       
        // Non-released contents cannot be target of relations
        if (!getStatus().equals(WGContent.STATUS_RELEASE)) {
            return Collections.emptyList();
        }
       
        return getDatabase().getCore().getIncomingRelations(getStructKey(), getLanguage().getName(), includeUnreleased);
       
    }
   
    public List<WGRelationData> getIncomingRelations() throws WGAPIException {
        return getIncomingRelations(false);
    }
   
    /**
     * Returns the data of a content relation
     * @param name Name of the relation
     * @return The relation data or null if the relation does not exist
     * @throws WGAPIException
     */
    public WGRelationData getRelationData(String name) throws WGAPIException {
       
        if (getDatabase().getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            return null;
        }
       
        return getCore().getRelationData(name);
       
    }
   
   
   
    /**
     * Assigns a new version number to this content
     * Used to resolve key conflicts with contents.
     * Note: The current WGContent object is INVALID after this call and MUST NOT be used for any further operations. It should be refetched from he database
     * @throws WGAPIException
     */
    public void reassignVersionNumber() throws WGAPIException {
       
        synchronized (getDatabase()) {

            // Find the next free version
            FreeContentVersionFinder finder =  getDatabase().new FreeContentVersionFinder(getStructEntry(), getLanguage());
            int newVersion = finder.findNewVersion();       
   
            // Unmap the content under its old keys.
            getDatabase().unmapDocumentObject(this);
            WGDocumentKey oldDocKey = getDocumentKeyObj();
           
            setMetaData(WGContent.META_VERSION, new Integer(newVersion));
           
            // We must keep the lastmodified date here, bc. there is a high change that this doc will
            // also be picked up by the multi released validation. When that happens, we STILL want
            // this version of the document to be the older one!
            save(getLastModified());
           
            // Changing version numbers of content documents is normally not supported so this WGContent object becomes invalid
            // It was remapped on save with new keys. We unmap it again to prevent it from being served again
            dropCore();
            getDatabase().unmapDocumentObject(this);
            getDatabase().getSessionContext().removeDocumentContext(oldDocKey);
            getDatabase().getSessionContext().removeDocumentContext(getDocumentKeyObj());
           

        }
       
    }

    @Override
    public int getType() {
        return WGDocument.TYPE_CONTENT;
    }
   
    protected void updateBackendCaches(WGDocumentCore core) {
        super.updateBackendCaches(core);
       
        try {
            getSessionData().setBackendStatus((String) core.getMetaData(WGContent.META_STATUS));
        }
        catch (WGAPIException e) {
            WGFactory.getLogger().error("Error updating struct entry backend cache", e);
        }
    }
   
    /**
     * Returns the status of this content that is currently stored to the content store.
     * This is especially helpful in status change events to determine the previous status of the document
     */
    public String getStoredStatus() {
        return getSessionData().getBackendStatus();
    }
   
    /**
     * Returns the names of all relations that belong to the given relation group
     * @param group
     * @return List of relation names
     */
    public List<String> getRelationNamesOfGroup(String group) throws WGAPIException {
       
        if (getDatabase().getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            return Collections.emptyList();
        }
       
        return getCore().getRelationNamesOfGroup(group);
       
    }


    /**
     * Returns the target documents of all relations that belong to the given relation group
     * @param group The group to query
     * @return List of target documents
     */
    public List<WGContent> getRelationsOfGroup(String group) throws WGAPIException  {
       
        List<WGContent> targets = new ArrayList<WGContent>();
        for (String relName : getRelationNamesOfGroup(group)) {
            WGContent con = getRelation(relName);
            if (con != null) {
                targets.add(con);
            }
        }
        return targets;
       
    }
   
    /**
     * Returns the data of all relations that belong to the given relation group
     * @param group The group to query
     * @return List of relation data objects
     */
    public List<WGRelationData> getRelationsDataOfGroup(String group) throws WGAPIException  {
       
        List<WGRelationData> targets = new ArrayList<WGRelationData>();
        for (String relName : getRelationNamesOfGroup(group)) {
            WGRelationData relData = getRelationData(relName);
            if (relData != null) {
                targets.add(relData);
            }
        }
        return targets;
       
    }
   
    /**
     * Creates a new relation and adds it to a relation group. The relation will be issued a random name that is returned by this method.
     * @param group The group of the relation.
     * @param target The target of the relation
     * @param relType The type of relation. Use constants RELATIONTYPE_*.
     * @return The name of the created relation
     * @throws WGAPIException
     */
    public String addRelationToGroup(String group, WGContent target, int relType) throws WGAPIException {
       
        if (getDatabase().getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGNotSupportedException("Relation groups are only supported in content stores of version 5 or higher");
        }
       
        String relName = group + "#" + UIDGenerator.generateUID();
        WGRelationData relation = new WGRelationData(getContentKey(true), relName, target.getStructKey(), target.getLanguage().getName(), relType, group);
        setRelation(relation);
        return relName;
    }
   
    /**
     * Removes the relation to a given target document from a relation group, if it contains such a relation.
     * If multiple relations point to the given target the method will remove all of them.
     * @param group The group
     * @param target The target document that should no longer be addressed by the group
     * @return true if a relation was found and removed, false otherwise
     * @throws WGAPIException
     */
    public boolean removeRelationFromGroup(String group, WGContent target) throws WGAPIException {
       
        if (getDatabase().getContentStoreVersion() < WGDatabase.CSVERSION_WGA5) {
            throw new WGNotSupportedException("Relation groups are only supported in content stores of version 5 or higher");
        }

       
        boolean somethingRemoved = false;
        for (String relName : getRelationNamesOfGroup(group)) {
            WGContent relTarget = getRelation(relName);
            if (target.equals(relTarget)) {
                removeRelation(relName);
                somethingRemoved = true;
            }
        }
        return somethingRemoved;
    }
   
    /**
     * Creates a new normal relation and adds it to a relation group. This is like calling {@link #addRelationToGroup(String, WGContent, int)} with relation type {@link #RELATIONTYPE_NORMAL}.
     * @param group The group of the relation.
     * @param target The target of the relation
     * @return The name of the created relation
     * @throws WGAPIException
     */
    public String addRelationToGroup(String group, WGContent target) throws WGAPIException {
        return addRelationToGroup(group, target, RELATIONTYPE_NORMAL);
    }
   
    /**
     * Removes all relations of the given group
     * @param group The relation group to remove
     * @throws WGAPIException
     */
    public void clearRelationGroup(String group) throws WGAPIException {
       
        List<WGRelationData> targets = new ArrayList<WGRelationData>();
        for (String relName : getRelationNamesOfGroup(group)) {
            removeRelation(relName);
        }
    }
   
    /**
     * Returns the names of all existing relation groups on this content document
     * @throws WGAPIException
     */
    public Set<String> getRelationGroups() throws WGAPIException {
       
        Set<String> groups = new HashSet<String>();
        for (String relName : (List<String>) getRelationNames()) {
            WGRelationData relData = getRelationData(relName);
            if (relData != null && relData.getGroup() != null) {
                groups.add(relData.getGroup());
            }
        }
        return groups;
       
    }
   
   
   
   
}
TOP

Related Classes of de.innovationgate.webgate.api.WGContent$SessionData

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.