/** * 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; } } }