/**
* Copyright 1999-2009 The Pegadi Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pegadi.server.article;
import no.dusken.common.model.Person;
import org.pegadi.articlesearch.JournalistTerm;
import org.pegadi.articlesearch.PhotographerTerm;
import org.pegadi.articlesearch.StatusTerm;
import org.pegadi.model.Article;
import org.pegadi.server.*;
import org.pegadi.server.user.UserServer;
import org.pegadi.sqlsearch.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Service;
import org.w3c.dom.*;
import javax.sql.DataSource;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.nio.charset.Charset;
import java.sql.*;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Date;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.pegadi.util.PersonUtils.getInitials;
/**
* @author Jørgen Binningsbø <jb@underdusken.no>
* @author Marvin B. Lillehaug <lillehau@underdusken.no>
*/
@Service
public class ArticleServerImpl implements ArticleServer {
private NamedParameterJdbcTemplate template;
private ArticleRowMapper mapper = new ArticleRowMapper();
/**
* The location for backup XML files.
*/
protected File backupLocation;
/**
* The location to articles in html
*/
protected File articleLocation;
/**
* The location to xml files
*/
protected File xmlLocation;
/**
* The URL to the articles saved in @see articleLocation
*/
protected String webXml;
/**
* An instance of a UserServer - needed to create the users connected
* to this article
*/
private UserServer userServer;
/**
* An instance of a PublicationServerImpl. Needed to create the <code>Publication</code>s
* connected to the articles.
*/
private PublicationServer publicationServer;
private ArticleTypeServer articleTypeServer;
private ArticleStatusServer articleStatusServer;
private SectionServer sectionServer;
private final Logger log = LoggerFactory.getLogger(getClass());
private String webBase;
private String webPath;
@Autowired
private DataSourceBasedLimitResultsUtil limitResultsUtil;
public ArticleServerImpl() {
}
public void setDataSource(DataSource dataSource) {
template = new NamedParameterJdbcTemplate(dataSource);
}
public void setUserServer(UserServer userServer) {
this.userServer = userServer;
}
public void setPublicationServer(PublicationServer publicationServer) {
this.publicationServer = publicationServer;
}
public void setArticleTypeServer(ArticleTypeServer articleTypeServer) {
this.articleTypeServer = articleTypeServer;
}
public void setSectionServer(SectionServer sectionServer) {
this.sectionServer = sectionServer;
}
public void init() {
log.debug("Backuplocation is: " + backupLocation);
if (!backupLocation.exists() || !backupLocation.isDirectory())
backupLocation.mkdirs();
if (!xmlLocation.exists() || !xmlLocation.isDirectory())
xmlLocation.mkdir();
setWebXml(webBase + webPath + webXml);
}
public boolean canEdit(Person person, int artId) {
// security-rules: the journalist can edit the article
// the photographer can edit the article. FIXME: He or she should only
// be allowed to view the article.
// the editor-in-chiefs can edit the article
// the proof-readers can edit the article
// co-journalists can only edit the xml, not permissions
Article article = getArticleByID(person, artId);
try {
if (userServer.isGod(person)) {
return true;
} else if (article.getPhotographer() != null && person.getUsername().equals(article.getPhotographer().getUsername())) {
return true;
} else if (article.getJournalist() != null && person.getUsername().equals(article.getJournalist().getUsername())) {
return true;
} else if (userServer.hasRole(19, person.getId())) { //proof-reader
return true;
} else {
Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put("article", artId);
parameters.put("journalist", person.getUsername());
int count = template.queryForInt("SELECT count(*) FROM CoJournalists WHERE refArticle=:article AND refJournalist=:journalist", parameters);
return count > 0;
}
} catch (Exception e) {
log.error("getArticleByID: error setting article security: ", e);
return false;
}
}
public boolean canDelete(Person person, int artID) {
Article art;
try {
art = getArticleByID(person, artID);
}
catch (Exception ee) {
log.error("canDelete: exception getting article", ee);
return false;
}
boolean isOwner = art.getJournalist() != null && art.getJournalist().getUsername() != null && art.getJournalist().getUsername().equals(person.getUsername());
return userServer.isGod(person) || isOwner;
}
public List<Article> getArticlesBySearchTerm(SearchTerm searchTerm) {
SearchTerm privateTerm = new OrTerm(new NotTerm(new StatusTerm(7)));
return searchWithPrivateArticleTerm(searchTerm, privateTerm);
}
public List<Article> getArticlesBySearchTerm(Person person, SearchTerm searchTerm) {
// Ensure private articles are not shown
SearchTerm privateTerm = new OrTerm(new NotTerm(new StatusTerm(7)), new OrTerm(new JournalistTerm(person), new PhotographerTerm(person)));
return searchWithPrivateArticleTerm(searchTerm, privateTerm);
}
private List<Article> searchWithPrivateArticleTerm(SearchTerm searchTerm, SearchTerm privateTerm) {
final AndTerm andTerm = new AndTerm(searchTerm, privateTerm);
andTerm.addTerm(new SearchTerm() {
public String whereClause() {
return "(Article.refArticleType=0 OR Article.refArticleType=ArticleType.ID)";
}
public List<String> getTables() {
return Arrays.asList("Article", "ArticleType");
}
});
andTerm.addTerm(new SearchTerm() {
public String whereClause() {
return "(Article.refStatus=0 OR Article.refStatus=ArticleStatus.ID)";
}
public List<String> getTables() {
return Arrays.asList("Article", "ArticleStatus");
}
});
andTerm.addTerm(new SearchTerm() {
public String whereClause() {
return "(Article.refSection=0 OR Article.refSection=Section.ID)";
}
public List<String> getTables() {
return Arrays.asList("Article", "Section");
}
});
andTerm.addTerm(new SearchTerm() {
public String whereClause() {
return "(Article.refPublication=Publication.ID)";
}
public List<String> getTables() {
return Arrays.asList("Article", "Publication");
}
});
andTerm.setSelectDistinct(true);
String query = andTerm.getQuery("Article.ID, Article.name, Article.refJournalist, " +
"Article.refPhotographer, Article.description, Article.characterCount, " +
"Article.wantedCharacters, Article.wantedPages, Article.lastSaved, Article.articlexml, " +
"Article.refPublication, Article.refArticleType, " +
"Article.refStatus, Article.refSection");
log.debug("Query is: {}", query);
return template.query(query, Collections.<String, Object>emptyMap(), mapper);
}
@Override
public List<Article> getArticlesBySearchTerm(SearchTerm searchTerm, Integer count, Integer offset) {
SearchTerm limitClauseToSearchTerm = limitResultsUtil.createLimitClauseToSearchTerm(searchTerm, count, offset);
String queryString = limitClauseToSearchTerm.getQuery("Article.*");
log.debug("Query is: {}", queryString);
return template.query(queryString, Collections.<String, Object>emptyMap(), mapper);
}
/**
* Deletes an article from the database
*
* @param articleID the <code>Article</code> object to be deleted
* @return <code>true</code> if the delete was successful.
*/
public boolean deleteArticle(int articleID) {
Map<String, Integer> parameters = Collections.singletonMap("id", articleID);
template.update("INSERT INTO DeletedArticle SELECT * from Article WHERE id = :id", parameters);
log.info("Copied article {} to DeletedArticle", articleID);
template.update("DELETE FROM Article WHERE ID=:id", parameters);
template.update("DELETE FROM CoJournalists WHERE refarticle=:id", parameters);
log.info("Deleted article " + articleID);
return true;
}
/**
* Saves a new or updates an existing article. If the articleID is 0, a new article is inserted into the database
* and the new ID is returned. Else the article will be updated. This method will not save or modyify
* the article text. The exception is when the article type changes.
*
* @param article The article to save.
* @return New articleID if new article is created or 0 for successful save of existing article. Returnvalue less than 0
* means that something was wrong, and the article was not successfully saved.
*/
public int saveArticle(final Article article) {
if (article == null) { // huh - no article?
log.error("error - can't save a non-exixting article...");
return -1;
} else {
log.info("Saving article with ID=" + article.getId());
final String journalist = article.getJournalist() != null ? article.getJournalist().getUsername() : null;
final String photographer = article.getPhotographer() != null ? article.getPhotographer().getUsername() : null;
final int publication = article.getPublication() != null ? article.getPublication().getId() : 0;
final int articleType = article.getArticleType() != null ? article.getArticleType().getId() : 0;
final String text = article.getText() != null ? article.getText() : "";
final int department = article.getSection() != null ? article.getSection().getId() : 0;
final int articlestatus = article.getArticleStatus() != null ? article.getArticleStatus().getId() : 1;
if (article.getId() == 0) { // no ID means insert a new article
KeyHolder keyHolder = new GeneratedKeyHolder();
final String insert = "INSERT INTO Article (name, refJournalist, refPhotographer, refPublication, description, refArticleType, wantedCharacters, wantedPages,articlexml, refSection, refStatus, lastSaved) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
template.getJdbcOperations().update(
new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps =
connection.prepareStatement(insert, new String[] {"ID"});
ps.setString(1, article.getName());
ps.setString(2, journalist);
ps.setString(3, photographer);
ps.setInt(4, publication);
ps.setString(5, article.getDescription());
ps.setInt(6, articleType);
ps.setInt(7, article.getWantedNumberOfCharacters());
ps.setFloat(8, article.getWantedNumberOfPages());
ps.setString(9, text);
ps.setInt(10, department);
ps.setInt(11, articlestatus);
ps.setTimestamp(12, new Timestamp((new Date()).getTime()));
return ps;
}
},
keyHolder);
int articleId = keyHolder.getKey().intValue();
log.info("Saved a new article, and gave it ID={}", articleId);
return articleId;
} else {
// Save existing article
int articletypeOld = template.queryForInt("SELECT refArticleType FROM Article WHERE ID=:id", Collections.singletonMap("id", article.getId()));
if (article.getArticleType() != null) {
int AT = article.getArticleType().getId();
// Important to do this before the article text is set
if (AT != articletypeOld && article.hasText()) {
changeArticleType(article, AT);
}
}
doBackup(article.getId(), article.getText());
Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put("name", article.getName());
parameters.put("text", article.getText());
parameters.put("department", department);
parameters.put("journalist", journalist);
parameters.put("photographer", photographer);
parameters.put("publication", publication);
parameters.put("description", article.getDescription());
parameters.put("wantedNumbeOfCharacters", article.getWantedNumberOfCharacters());
parameters.put("wantedNumberOfPages", article.getWantedNumberOfPages());
parameters.put("articleType", articleType);
parameters.put("articleStatus", articlestatus);
parameters.put("lastSaved", new Timestamp((new Date()).getTime()));
parameters.put("id", article.getId());
template.update("UPDATE Article " +
"SET name=:name, articlexml=:text, refSection=:department, " +
"refJournalist=:journalist, refPhotographer=:photographer, " +
"refPublication=:publication, description=:description, " +
"wantedCharacters=:wantedNumbeOfCharacters, wantedPages=:wantedNumberOfPages, " +
"refArticleType=:articleType, refStatus=:articleStatus, " +
"lastSaved=:lastSaved " +
"WHERE ID=:id", parameters);
}
}
return 0;
}
/**
* Change the main tag name of an article. Used when the article type changes.
*
* @param art The article.
* @param ID The ID of the new article type.
*/
protected void changeArticleType(Article art, int ID) {
Map<String, Object> map = template.queryForMap("SELECT tagname FROM ArticleType WHERE ID=:id", Collections.singletonMap("id", ID));
String tag = (String) map.get("tagname");
Document doc = art.getDocument();
if (doc == null) {
art.parseText();
doc = art.getDocument();
}
if (doc == null) {
log.error("changeArticleType: Can't get document for article, article is NOT changed.");
return;
}
Element root = doc.getDocumentElement();
log.info("Root before: " + doc.getDocumentElement().toString());
Element replace = doc.createElement(tag);
NamedNodeMap att = root.getAttributes();
for (int i = 0; i < att.getLength(); i++) {
Node n = att.item(i);
if (n instanceof Attr) {
replace.setAttribute(((Attr) n).getName(), ((Attr) n).getValue());
}
}
NodeList nl = root.getChildNodes();
log.info("changeArticleType: Root node has {} children.", nl.getLength());
for (int i = 0; i < nl.getLength(); i++) {
Node clone = nl.item(i).cloneNode(true);
log.info("Adding node {} to replace", (i + 1));
replace.appendChild(clone);
}
log.info("Replacement node: {}", replace.toString());
doc.replaceChild(replace, root);
log.info("Root after: {}", doc.getDocumentElement().toString());
if (!art.serialize()) {
log.error("changeArticleType: Can't serialize the changed XML.");
}
}
/**
* Save the text of an open article.
*
* @param article The article.
*/
public boolean saveArticleText(Article article) {
boolean retval = false;
doBackup(article.getId(), article.getText());
try {
Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put("text", article.getText());
parameters.put("lastSaved", new Timestamp((new Date()).getTime()));
parameters.put("currentNumberOfCharacters", article.getCurrentNumberOfCharacters());
parameters.put("id", article.getId());
template.update("UPDATE Article SET articlexml=:text, lastSaved=:lastSaved, characterCount=:currentNumberOfCharacters WHERE ID=:id", parameters);
log.info("saveArticleText: Article with ID {} saved.", article.getId());
retval = true;
} catch (DataAccessException e) {
log.error("saveArticleText: SQL exception saving article text for art.ID {}", article.getId(), e);
}
return retval;
}
public Article getArticleByID(Person person, int ID) {
log.info("getArticleByID: Getting Article with ID {} for user {}", ID, person.getUsername());
String query = "SELECT * FROM Article WHERE ID=:id AND (refStatus!=7 OR refJournalist=:journalist)";
Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put("id", ID);
parameters.put("journalist", person.getUsername());
try {
return template.queryForObject(query, parameters, mapper);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
public Article getArticleByID(int ID) {
log.info("getArticleByID: Getting Article with ID {}", ID);
String query = "SELECT * FROM Article WHERE ID=:id AND (refStatus<>7)";
try {
return template.queryForObject(query, Collections.singletonMap("id", ID), mapper);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
public List<Article> getArticlesByPageID(int pageId) {
try {
return template.query("SELECT * FROM Article JOIN disppagearticles on disppagearticles.articleid = Article.id WHERE disppagearticles.pageid =:pageId", Collections.singletonMap("pageId", pageId), mapper);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
public List<Article> getArticlesByDispID(int dispId) {
try {
return template.query("SELECT * FROM Article JOIN Publication on Article.refpublication=Publication.id join disp on disp.publicationid=Publication.id where disp.id=:id", Collections.singletonMap("id", dispId),mapper);
} catch (EmptyResultDataAccessException e) {
return null;
}
}
/**
* Backup an article as XML
*
* @param articleID The ID of the article.
* @param text The text of the article.
* @return <code>true</code> if all parts of the save operation
* was successful.
*/
protected boolean doBackup(int articleID, String text) {
// Dump the XML to file
SimpleDateFormat df = new SimpleDateFormat("-yyyyy-MM-dd-HH-mm");
String xmlFile = backupLocation + "/" + articleID + df.format(new Date()) + ".xml";
FileOutputStream fos = null;
OutputStreamWriter writer = null;
try {
fos = new FileOutputStream(xmlFile);
writer = new OutputStreamWriter(fos, Charset.forName("utf-8"));
writer.write(text);
} catch (IOException e) {
log.error("doBackup: Error writing XML file, aborting publish", e);
log.info("saveArticleText: WARNING: backup failed for article {}!", articleID);
}finally {
try {
if (writer != null) writer.close();
if (fos != null ) fos.close();
} catch (IOException e) {
log.error("Failed on stream close", e);
}
}
log.info("doBackup: XML file saves as '{}'.", xmlFile);
return true;
}
/**
* Returns the template for the given article. This method will return
* <code>null</code> if the template isn't found or an exception is thrown.
* TODO: This method does not consider the encoding of the template XML.
* The proper way to fix this is to parse as XML, and then serialize.
*
* @param articleID The ID of the article.
* @return The template.
*/
public String getTemplate(int articleID) {
Map parameters = template.queryForMap("SELECT Article.refJournalist, Article.refPhotographer, ArticleType.template FROM Article, ArticleType WHERE Article.ID=:id AND ArticleType.ID=Article.refArticleType", Collections.singletonMap("id", articleID));
String templName = (String) parameters.get("template");
String journalistID = (String) parameters.get("refJournalist");
String photographerID = (String) parameters.get("refPhotographer");
log.info("getTemplate: Retrieving template '{}/{}'.", xmlLocation, templName);
String template;
try {
StringWriter temp = new StringWriter();
TransformerFactory fac = TransformerFactory.newInstance();
Transformer trans = fac.newTransformer();
trans.transform(new StreamSource(new FileInputStream(xmlLocation + "/" + templName)), new StreamResult(temp));
template = temp.toString();
} catch (Exception e) {
log.error("getTemplate: Exception reading template file", e);
return null;
}
// Format the template
MessageFormat mf = new MessageFormat(template);
Object[] args = new Object[10];
log.debug("setting webxml to {}", webXml);
args[0] = webXml;
args[9] = articleID;
// Set journalist info for template
Person j = null;
try {
if (isNotBlank(journalistID) ) {
j = userServer.getUserByUsername(journalistID);
}
} catch (Exception e) {
log.error("getTemplate: Error getting journalist info", e);
}
if (j != null) {
args[1] = j.getUsername();
args[2] = j.getName();
args[3] = j.getEmailAddress();
args[4] = getInitials(j);
} else {
args[1] = 0;
args[2] = "";
args[3] = "";
args[4] = "";
}
// Set photographer info for template
Person p = null;
try {
if (isNotBlank(photographerID)) {
p = userServer.getUserByUsername(photographerID);
}
} catch (Exception e) {
log.error("getTemplate: Error getting photographer info", e);
}
if (p != null) {
args[5] = p.getUsername();
args[6] = p.getName();
args[7] = p.getEmailAddress();
args[8] = getInitials(p);
} else {
args[5] = 0;
args[6] = "";
args[7] = "";
args[8] = "";
}
try {
return mf.format(args);
} catch (Exception e) {
log.error("getTemplate: Error formatting template", e);
return null;
}
}
public List<Person> getCoJournalistsForArticle(int articleID) {
return template.query("SELECT refJournalist FROM CoJournalists WHERE refArticle=:article", Collections.singletonMap("article", articleID), new RowMapper<Person>() {
@Override
public Person mapRow(ResultSet rs, int rowNum) throws SQLException {
return userServer.getUserByUsername(rs.getString("refJournalist"));
}
});
}
public void setCoJournalistsForArticle(int articleID, List<String> coJournalists) {
template.update("DELETE FROM CoJournalists WHERE refArticle=:articleID", Collections.singletonMap("articleID", articleID));
HashMap<String, Object> parameters = new HashMap<String, Object>();
parameters.put("articleID", articleID);
for(String id : coJournalists){
parameters.put("coJournalist",id);
template.update("INSERT INTO CoJournalists VALUES(:articleID, :coJournalist)", parameters);
}
}
public void setWebBase(String webBase) {
this.webBase = webBase;
}
public void setWebPath(String webPath) {
// add / to path
if(webPath.length() != 0){
webPath = webPath + "/";
}
this.webPath = webPath;
}
public File getArticleLocation() {
return articleLocation;
}
public void setArticleLocation(File v) {
this.articleLocation = v;
log.info("Article location set to: " + v);
}
public File getBackupLocation() {
return backupLocation;
}
public void setBackupLocation(File location) {
this.backupLocation = location;
log.info("Backup location set to " + backupLocation);
}
public File getXmlLocation() {
return xmlLocation;
}
public void setXmlLocation(File v) {
this.xmlLocation = v;
log.info("XML location set to: " + v);
}
public String getWebXml() {
return webXml;
}
public void setWebXml(String v) {
this.webXml = v;
log.info("Web XML set to: ", v);
}
public void setArticleStatusServer(ArticleStatusServer articleStatusServer) {
this.articleStatusServer = articleStatusServer;
}
private class ArticleRowMapper implements RowMapper<Article> {
public Article mapRow(ResultSet rs, int rowNum) throws SQLException {
Article article = new Article(rs.getInt("ID"));
article.setName(rs.getString("name"));
String refJournalist = rs.getString("refJournalist");
if (isNotBlank(refJournalist)) {
article.setJournalist(userServer.getUserByUsername(refJournalist));
}
String refPhotographer = rs.getString("refPhotographer");
if (isNotBlank(refPhotographer)) {
article.setPhotographer(userServer.getUserByUsername(refPhotographer));
}
article.setDescription(rs.getString("description"));
article.setCurrentNumberOfCharacters(rs.getInt("characterCount"));
article.setWantedNumberOfCharacters(rs.getInt("wantedCharacters"));
article.setWantedNumberOfPages(rs.getFloat("wantedPages"));
article.setLastSaved(new Date(rs.getTimestamp("lastSaved").getTime()));
article.setText(rs.getString("articlexml"));
article.setPublication(publicationServer.getPublicationByID(rs.getInt("refPublication")));
article.setArticleType(articleTypeServer.getArticleType(rs.getInt("refArticleType")));
article.setArticleStatus(articleStatusServer.getArticleStatus(rs.getInt("refStatus")));
article.setSection(sectionServer.getDepartment(rs.getInt("refSection")));
return article;
}
}
}