/**************************************************************************************** * Copyright (c) 2009 Daniel Svärd <daniel.svard@gmail.com> * * Copyright (c) 2010 Rick Gruber-Riemer <rick@vanosten.net> * * * * This program 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. * * * * This program 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 * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.ichi2.anki.model; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ichi2.anki.Utils; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Template; /** * Card model. Card models are used to make question/answer pairs for the information you add to facts. You can display * any number of fields on the question side and answer side. * * @see http://ichi2.net/anki/wiki/ModelProperties#Card_Templates */ public class CardModel implements Comparator<CardModel> { private static Logger log = LoggerFactory.getLogger(CardModel.class); // TODO: Javadoc. // TODO: Methods for reading/writing from/to DB. public static final int DEFAULT_FONT_SIZE = 20; public static final int DEFAULT_FONT_SIZE_RATIO = 100; public static final String DEFAULT_FONT_FAMILY = "Arial"; public static final String DEFAULT_FONT_COLOR = "#000000"; public static final String DEFAULT_BACKGROUND_COLOR = "#FFFFFF"; /** Regex pattern used in removing tags from text before diff */ // private static final Pattern sFactPattern = Pattern.compile("%\\([tT]ags\\)s"); // private static final Pattern sModelPattern = Pattern.compile("%\\(modelTags\\)s"); // private static final Pattern sTemplPattern = Pattern.compile("%\\(cardModel\\)s"); // Regex pattern for converting old style template to new private static final Pattern sOldStylePattern = Pattern.compile("%\\((.+?)\\)s"); // BEGIN SQL table columns private long mId; // Primary key private int mOrdinal; private long mModelId; // Foreign key models.id private String mName; private String mDescription = ""; private int mActive = 1; // Formats: question/answer/last (not used) private String mQformat; private String mAformat; // private String mLformat; // Question/answer editor format (not used yet) // private String mQedformat; // private String mAedformat; private int mQuestionInAnswer = 0; // Unused private String mQuestionFontFamily = DEFAULT_FONT_FAMILY; private int mQuestionFontSize = DEFAULT_FONT_SIZE; private String mQuestionFontColour = DEFAULT_FONT_COLOR; // Used for both question & answer private int mQuestionAlign = 0; // Unused private String mAnswerFontFamily = DEFAULT_FONT_FAMILY; private int mAnswerFontSize = DEFAULT_FONT_SIZE; private String mAnswerFontColour = DEFAULT_FONT_COLOR; private int mAnswerAlign = 0; // private String mLastFontFamily = DEFAULT_FONT_FAMILY; // private int mLastFontSize = DEFAULT_FONT_SIZE; // Used as background colour private String mLastFontColour = DEFAULT_BACKGROUND_COLOR; // private String mEditQuestionFontFamily = ""; // private int mEditQuestionFontSize = 0; // private String mEditAnswerFontFamily = ""; // private int mEditAnswerFontSize = 0; // Empty answer // private int mAllowEmptyAnswer = 1; private String mTypeAnswer = ""; // END SQL table entries // Compiled mustache templates private Template mQTemplate = null; private Template mATemplate = null; /** * Backward reference */ // private Model mModel; /** * Constructor. */ public CardModel(String name, String qformat, String aformat, boolean active) { mName = name; mQformat = qformat; mAformat = aformat; mActive = active ? 1 : 0; mId = Utils.genID(); } /** * Constructor. */ public CardModel() { this("", "q", "a", true); } /** SELECT string with only those fields, which are used in AnkiDroid */ private static final String SELECT_STRING = "SELECT id, ordinal, modelId, name, description, active, qformat, " + "aformat, questionInAnswer, questionFontFamily, questionFontSize, questionFontColour, questionAlign, " + "answerFontFamily, answerFontSize, answerFontColour, answerAlign, lastFontColour, typeAnswer" + " FROM cardModels"; /** * @param modelId * @param models will be changed by adding all found CardModels into it * @return unordered CardModels which are related to a given Model and eventually active put into the parameter * "models" */ protected static final void fromDb(Deck deck, long modelId, LinkedHashMap<Long, CardModel> models) { ResultSet result = null; CardModel myCardModel = null; try { StringBuffer query = new StringBuffer(SELECT_STRING); query.append(" WHERE modelId = "); query.append(modelId); query.append(" ORDER BY ordinal"); result = deck.getDB().rawQuery(query.toString()); while (result.next()) { myCardModel = new CardModel(); int i = 1; myCardModel.mId = result.getLong(i++); myCardModel.mOrdinal = result.getInt(i++); myCardModel.mModelId = result.getLong(i++); myCardModel.mName = result.getString(i++); myCardModel.mDescription = result.getString(i++); myCardModel.mActive = result.getInt(i++); myCardModel.mQformat = result.getString(i++); myCardModel.mAformat = result.getString(i++); myCardModel.mQuestionInAnswer = result.getInt(i++); myCardModel.mQuestionFontFamily = result.getString(i++); myCardModel.mQuestionFontSize = result.getInt(i++); myCardModel.mQuestionFontColour = result.getString(i++); myCardModel.mQuestionAlign = result.getInt(i++); myCardModel.mAnswerFontFamily = result.getString(i++); myCardModel.mAnswerFontSize = result.getInt(i++); myCardModel.mAnswerFontColour = result.getString(i++); myCardModel.mAnswerAlign = result.getInt(i++); myCardModel.mLastFontColour = result.getString(i++); myCardModel.mTypeAnswer = result.getString(i++); myCardModel.refreshTemplates(); models.put(myCardModel.mId, myCardModel); } } catch (SQLException e) { e.printStackTrace(); } finally { try { if (result != null) { result.close(); } } catch (SQLException e) { } } } protected void toDB(Deck deck) { Map<String, Object> values = new HashMap<String, Object>(); values.put("id", mId); values.put("ordinal", mOrdinal); values.put("modelId", mModelId); values.put("name", mName); values.put("description", mDescription); values.put("active", mActive); values.put("qformat", mQformat); values.put("aformat", mAformat); values.put("questionInAnswer", mQuestionInAnswer); values.put("questionFontFamily", mQuestionFontFamily); values.put("questionFontSize", mQuestionFontSize); values.put("questionFontColour", mQuestionFontColour); values.put("questionAlign", mQuestionAlign); values.put("answerFontFamily", mAnswerFontFamily); values.put("answerFontSize", mAnswerFontSize); values.put("answerFontColour", mAnswerFontColour); values.put("answerAlign", mAnswerAlign); values.put("lastFontColour", mLastFontColour); deck.getDB().update(deck, "cardModels", values, "id = " + mId); } public boolean isActive() { return (mActive != 0); } /** * This function recompiles the templates for question and answer. It should be called everytime we change mQformat * or mAformat, so if in the future we create set(Q|A)Format setters, we should include a call to this. */ private void refreshTemplates() { // Question template StringBuffer sb = new StringBuffer(); Matcher m = sOldStylePattern.matcher(mQformat); while (m.find()) { // Convert old style m.appendReplacement(sb, "{{" + m.group(1) + "}}"); } m.appendTail(sb); log.info("Compiling question template \"" + sb.toString() + "\""); mQTemplate = Mustache.compiler().compile(sb.toString()); // Answer template sb = new StringBuffer(); m = sOldStylePattern.matcher(mAformat); while (m.find()) { // Convert old style m.appendReplacement(sb, "{{" + m.group(1) + "}}"); } m.appendTail(sb); log.info("Compiling answer template \"" + sb.toString() + "\""); mATemplate = Mustache.compiler().compile(sb.toString()); } /** * @param cardModelId * @return the modelId for a given cardModel or 0, if it cannot be found */ protected static final long modelIdFromDB(Deck deck, long cardModelId) { ResultSet result = null; long modelId = -1; try { String query = "SELECT modelId FROM cardModels WHERE id = " + cardModelId; result = deck.getDB().rawQuery(query); if (result.next()) { modelId = result.getLong(1); } } catch (SQLException e) { e.printStackTrace(); } finally { try { if (result != null) { result.close(); } } catch (SQLException e) { } } return modelId; } public static HashMap<String, String> formatQA(Fact fact, CardModel cm, String[] tags) { Map<String, String> fields = new HashMap<String, String>(); for (Fact.Field f : fact.getFields()) { fields.put("text:" + f.getFieldModel().getName(), Utils.stripHTML(f.getValue())); if (!f.getValue().equals("")) { fields.put(f.getFieldModel().getName(), String.format("<span class=\"fm%s\">%s</span>", Utils .hexifyID(f.getFieldModelId()), f.getValue())); } else { fields.put(f.getFieldModel().getName(), ""); } } fields.put("tags", tags[Card.TAGS_FACT]); fields.put("Tags", tags[Card.TAGS_FACT]); fields.put("modelTags", tags[Card.TAGS_MODEL]); fields.put("cardModel", tags[Card.TAGS_TEMPL]); HashMap<String, String> d = new HashMap<String, String>(); d.put("question", cm.mQTemplate.execute(fields)); d.put("answer", cm.mATemplate.execute(fields)); return d; } /* private static String replaceField(String replaceFrom, Fact fact, int replaceAt, boolean isQuestion) { int endIndex = replaceFrom.indexOf(")", replaceAt); String fieldName = replaceFrom.substring(replaceAt + 2, endIndex); char fieldType = replaceFrom.charAt(endIndex + 1); if (isQuestion) { String replace = "%(" + fieldName + ")" + fieldType; String with = "<span class=\"fm" + Long.toHexString(fact.getFieldModelId(fieldName)) + "\">" + fact.getFieldValue(fieldName) + "</span>"; replaceFrom = replaceFrom.replace(replace, with); } else { replaceFrom.replace("%(" + fieldName + ")" + fieldType, "<span class=\"fma" + Long.toHexString(fact.getFieldModelId(fieldName)) + "\">" + fact.getFieldValue(fieldName) + "</span"); } return replaceFrom; } private static String replaceHtmlField(String replaceFrom, Fact fact, int replaceAt) { int endIndex = replaceFrom.indexOf(")", replaceAt); String fieldName = replaceFrom.substring(replaceAt + 7, endIndex); char fieldType = replaceFrom.charAt(endIndex + 1); String replace = "%(text:" + fieldName + ")" + fieldType; String with = fact.getFieldValue(fieldName); replaceFrom = replaceFrom.replace(replace, with); return replaceFrom; } */ /** * Implements Comparator by comparing the field "ordinal". * * @param object1 * @param object2 * @return */ public int compare(CardModel object1, CardModel object2) { return object1.mOrdinal - object2.mOrdinal; } /** * @return the id */ public long getId() { return mId; } /** * @return the ordinal */ public int getOrdinal() { return mOrdinal; } /** * @return the questionInAnswer */ public boolean isQuestionInAnswer() { // FIXME hmmm, is that correct? return (mQuestionInAnswer == 0); } /** * @return the lastFontColour */ public String getLastFontColour() { return mLastFontColour; } /** * @return the questionFontFamily */ public String getQuestionFontFamily() { return mQuestionFontFamily; } /** * @return the questionFontSize */ public int getQuestionFontSize() { return mQuestionFontSize; } /** * @return the questionFontColour */ public String getQuestionFontColour() { return mQuestionFontColour; } /** * @return the questionAlign */ public int getQuestionAlign() { return mQuestionAlign; } /** * @return the answerFontFamily */ public String getAnswerFontFamily() { return mAnswerFontFamily; } /** * @return the answerFontSize */ public int getAnswerFontSize() { return mAnswerFontSize; } /** * @return the answerFontColour */ public String getAnswerFontColour() { return mAnswerFontColour; } /** * @return the answerAlign */ public int getAnswerAlign() { return mAnswerAlign; } /** * @return the name */ public String getName() { return mName; } /** * Getter for question Format * @return the question format */ public String getQFormat() { return mQformat; } /** * Setter for question Format * @param the new question format */ public void setQFormat(String qFormat) { mQformat = qFormat; } /** * Getter for answer Format * @return the answer format */ public String getAFormat() { return mAformat; } /** * Setter for answer Format * @param the new answer format */ public void setAFormat(String aFormat) { mAformat = aFormat; } public long getModelId() { return mModelId; } public String getTypeAnswer() { return mTypeAnswer; } }