Project: zanata
/*
 * Copyright 2010, Red Hat, Inc. and individual contributors as indicated by the 
 * @author tags. See the copyright.txt file in the distribution for a full 
 * listing of individual contributors. 
 *  
 * This is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free 
 * Software Foundation; either version 2.1 of the License, or (at your option) 
 * any later version. 
 *  
 * This software 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 Lesser General Public License for more 
 * details. 
 *  
 * You should have received a copy of the GNU Lesser General Public License 
 * along with this software; if not, write to the Free Software Foundation, 
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF 
 * site: http://www.fsf.org. 
 */
package org.zanata.model; 
 
import java.io.Serializable; 
import java.util.ArrayList; 
import java.util.HashMap; 
import java.util.List; 
import java.util.Map; 
import javax.persistence.CascadeType; 
import javax.persistence.Column; 
import javax.persistence.Entity; 
import javax.persistence.FetchType; 
import javax.persistence.GeneratedValue; 
import javax.persistence.Id; 
import javax.persistence.JoinColumn; 
import javax.persistence.JoinTable; 
import javax.persistence.ManyToOne; 
import javax.persistence.MapKey; 
import javax.persistence.NamedQueries; 
import javax.persistence.NamedQuery; 
import javax.persistence.OneToMany; 
import javax.persistence.OneToOne; 
import javax.persistence.PostLoad; 
import javax.persistence.PostPersist; 
import javax.persistence.PostUpdate; 
import javax.persistence.PreUpdate; 
 
import org.hibernate.annotations.AccessType; 
import org.hibernate.annotations.BatchSize; 
import org.hibernate.annotations.Cache; 
import org.hibernate.annotations.CacheConcurrencyStrategy; 
import org.hibernate.annotations.Cascade; 
import org.hibernate.annotations.CollectionOfElements; 
import org.hibernate.annotations.IndexColumn; 
import org.hibernate.annotations.NaturalId; 
import org.hibernate.annotations.Type; 
import org.hibernate.search.annotations.Field; 
import org.hibernate.search.annotations.FieldBridge; 
import org.hibernate.search.annotations.Index; 
import org.hibernate.search.annotations.Indexed; 
import org.hibernate.validator.Length; 
import org.hibernate.validator.NotEmpty; 
import org.hibernate.validator.NotNull; 
import org.zanata.common.HasContents; 
import org.zanata.common.LocaleId; 
import org.zanata.hibernate.search.ContainingWorkspaceBridge; 
import org.zanata.model.po.HPotEntryData; 
import org.zanata.util.HashUtil; 
import org.zanata.util.OkapiUtil; 
import org.zanata.util.StringUtil; 
import com.google.common.base.Objects; 
 
import lombok.AccessLevel; 
import lombok.NoArgsConstructor; 
import lombok.Setter; 
import lombok.ToString; 
import lombok.extern.slf4j.Slf4j; 
 
/**
 * Represents a flow of source text that should be processed as a stand-alone 
 * structural unit. 
 *  
 * @see org.zanata.rest.dto.resource.TextFlow 
 * @author Asgeir Frimannsson <[email protected]
 *  
 */
 
@Entity 
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) 
@Indexed 
@NamedQueries(@NamedQuery( 
      name = "HTextFlow.findIdsWithTranslations"
      query = "SELECT tft.textFlow.id FROM HTextFlowTarget tft " + 
            "WHERE tft.locale.localeId=:locale " + 
            "AND tft.state=org.zanata.common.ContentState.Approved " + 
            "AND tft.textFlow.document.projectIteration.status<>org.zanata.common.EntityStatus.OBSOLETE " + 
            "AND tft.textFlow.document.projectIteration.project.status<>org.zanata.common.EntityStatus.OBSOLETE" 
)) 
@Setter 
@NoArgsConstructor 
@ToString(of = {"resId""revision""contents""comment""obsolete"}) 
@Slf4j 
public class HTextFlow extends HTextContainer implements Serializable, ITextFlowHistory, HasSimpleComment, HasContents 
   private static final long serialVersionUID = 3023080107971905435L
 
   private Long id; 
 
   private Integer revision = 1
 
   private String resId; 
 
   @Setter(AccessLevel.PROTECTED) 
   private Integer pos; 
 
   private HDocument document; 
 
   private boolean obsolete = false
 
   private List<String> contents; 
 
   private Map<Long, HTextFlowTarget> targets; 
 
   private Map<Integer, HTextFlowHistory> history; 
 
   private HSimpleComment comment; 
 
   private HPotEntryData potEntryData; 
    
   private Long wordCount; 
    
   private String contentHash; 
    
   private boolean plural; 
 
   // Only for internal use (persistence transient) 
   @Setter(AccessLevel.PRIVATE) 
   private Integer oldRevision; 
    
   // Only for internal use (persistence transient) 
   @Setter(AccessLevel.PRIVATE) 
   private HTextFlowHistory initialState; 
    
   // Only for internal use (persistence transient) 
   @Setter(AccessLevel.PRIVATE) 
   private boolean lazyRelationsCopied = false
 
   public HTextFlow(HDocument document, String resId, String content) 
   { 
      setDocument(document); 
      setResId(resId); 
      setContents(content); 
   } 
 
 
   @Id 
   @GeneratedValue 
   public Long getId() 
   { 
      return id; 
   } 
 
   protected void setId(Long id) 
   { 
      this.id = id; 
   } 
 
   // we can't use @NotNull because the position isn't set until the object has 
   // been persisted 
   @Column(insertable = false, updatable = false, nullable = false
   // @Column(insertable=false, updatable=false) 
   @Override 
   public Integer getPos() 
   { 
      return pos; 
   } 
 
   // TODO make this case sensitive 
   // TODO PERF @NaturalId(mutable=false) for better criteria caching 
   @NaturalId 
   @Length(max = 255
   @NotEmpty 
   public String getResId() 
   { 
      return resId; 
   } 
 
   /**
    * @return whether this message supports plurals 
    */
 
   public boolean isPlural() 
   { 
      return plural; 
   } 
 
   @NotNull 
   @Override 
   public Integer getRevision() 
   { 
      return revision; 
   } 
 
   @Override 
   public boolean isObsolete() 
   { 
      return obsolete; 
   } 
 
   /**
    * Caller must ensure that textFlow is in document.textFlows if and only if 
    * obsolete = false 
    *  
    * @param obsolete 
    */
 
   public void setObsolete(boolean obsolete) 
   { 
      this.obsolete = obsolete; 
   } 
 
   @ManyToOne 
   @JoinColumn(name = "document_id", insertable = false, updatable = false, nullable = false
   // TODO PERF @NaturalId(mutable=false) for better criteria caching 
   @NaturalId 
   @AccessType("field"
   @Field(index = Index.UN_TOKENIZED) 
   @FieldBridge(impl = ContainingWorkspaceBridge.class
   public HDocument getDocument() 
   { 
      return document; 
   } 
 
   public void setDocument(HDocument document) 
   { 
      if (!Objects.equal(this.document, document)) 
      { 
         this.document = document; 
         updateWordCount(); 
      } 
   } 
 
   // TODO use orphanRemoval=true: requires JPA 2.0 
   @OneToOne(optional = true, fetch = FetchType.LAZY, cascade = CascadeType.ALL) 
   @Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN) 
   @JoinColumn(name = "comment_id"
   public HSimpleComment getComment() 
   { 
      return comment; 
   } 
 
   @Override 
   @NotEmpty 
   @Type(type = "text"
   @AccessType("field"
   @CollectionOfElements(fetch = FetchType.EAGER) 
   @JoinTable(name = "HTextFlowContent",  
      joinColumns = @JoinColumn(name = "text_flow_id"
   ) 
   @IndexColumn(name = "pos", nullable = false
   @Column(name = "content", nullable = false
   public List<String> getContents() 
   { 
      // Copy lazily loaded relations to the history object as this cannot be done 
      // in the entity callbacks 
      copyLazyLoadedRelationsToHistory(); 
       
      if( contents == null ) 
      { 
         contents = new ArrayList<String>(); 
      } 
      return contents; 
   } 
 
   public void setContents(List<String> contents) 
   { 
      // Copy lazily loaded relations to the history object as this cannot be done 
      // in the entity callbacks 
      copyLazyLoadedRelationsToHistory(); 
 
      if (!Objects.equal(this.contents, contents)) 
      { 
         this.contents = new ArrayList<String>(contents); 
         updateWordCount(); 
         updateContentHash(); 
      } 
   } 
 
   @OneToMany(cascade = {CascadeType.REMOVE, CascadeType.MERGE, CascadeType.PERSIST}, mappedBy = "textFlow"
   @MapKey(name = "revision"
   public Map<Integer, HTextFlowHistory> getHistory() 
   { 
      ifthis.history == null ) 
      { 
         this.history = new HashMap<Integer, HTextFlowHistory>(); 
      } 
      return history; 
   } 
 
   @OneToMany(cascade = CascadeType.ALL, mappedBy = "textFlow"
   @org.hibernate.annotations.MapKey( columns = { 
         @Column(name = "locale"
   }) 
   @BatchSize(size = 10
   @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) 
   public Map<Long, HTextFlowTarget> getTargets() 
   { 
      if (targets == null
      { 
         targets = new HashMap<Long, HTextFlowTarget>(); 
      } 
      return targets; 
   } 
 
   @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true
   public HPotEntryData getPotEntryData() 
   { 
      return potEntryData; 
   } 
 
   @NotNull 
   public Long getWordCount() 
   { 
      return wordCount; 
   } 
 
   // this method is private because setContent(), and only setContent(), should 
   // be setting wordCount 
   private void setWordCount(Long wordCount) 
   { 
      this.wordCount = wordCount; 
   } 
    
   public String getContentHash() 
   { 
      return contentHash; 
   } 
    
   // this method is private because setContent(), and only setContent(), should 
   // be setting the contentHash 
   private void setContentHash(String contentHash) 
   { 
      this.contentHash = contentHash; 
   } 
 
   private void updateWordCount() 
   { 
      if (document == null || contents == null
      { 
         // come back when the not-null constraints are satisfied! 
         return
      } 
      String locale = toBCP47(document.getLocale()); 
      // TODO strip (eg) HTML tags before counting words. Needs more metadata 
      // about the content type. 
      long count = 0
      for( String content : this.getContents() ) 
      { 
         count += OkapiUtil.countWords(content, locale); 
      } 
      setWordCount(count); 
   } 
    
   private void updateContentHash() 
   { 
      String contents = StringUtil.concat(getContents(), '|'); 
      this.setContentHash(HashUtil.generateHash(contents)); 
   } 
 
   private String toBCP47(HLocale hLocale) 
   { 
      HLocale docLocale = document.getLocale(); 
      if (docLocale == null
      { 
         // *should* only happen in tests 
         log.warn("null locale, assuming 'en'"); 
         return "en"
      } 
      LocaleId docLocaleId = docLocale.getLocaleId(); 
      return docLocaleId.getId(); 
   } 
    
   @PreUpdate 
   private void preUpdate() 
   { 
      if( !this.revision.equals(this.oldRevision) ) 
      { 
         // there is an initial state 
         ifthis.initialState != null ) 
         { 
            this.getHistory().put(this.oldRevision, this.initialState); 
         } 
      } 
   } 
    
   @PostUpdate 
   @PostPersist 
   @PostLoad 
   private void updateInternalHistory() 
   { 
      this.oldRevision = this.revision; 
      this.initialState = new HTextFlowHistory(this); 
      this.lazyRelationsCopied = false
   } 
    
   /**
    * Copies all lazy loaded relations to the history object. 
    */
 
   private void copyLazyLoadedRelationsToHistory() 
   { 
      ifthis.initialState != null && this.initialState.getContents() == null && !this.lazyRelationsCopied ) 
      { 
         this.initialState.setContents( this.contents ); 
         this.lazyRelationsCopied = true
      } 
   } 
 
}