/* * Copyright 2012 Axel Winkler, Daniel Dunér * * This file is part of Daxplore Presenter. * * Daxplore Presenter 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. * * Daxplore Presenter 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 Daxplore Presenter. If not, see <http://www.gnu.org/licenses/>. */ package org.daxplore.presenter.shared; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import org.daxplore.presenter.shared.EmbedDefinition.EmbedFlag; /** * The Query Definition is a universal specification of a chart's content. * * <p>It defines what question, perspective and perspective options to display * and what chart type should be used.</p> * * <p>The definition contains: * <ul> * <li><b>questionID</b>, which specifies the ID of the question to be used * as the question</li> * <li><b>perspectiveID</b>, which specifies the ID of the question to be used * as the perspective</li> * <li><b>usedPerspectiveOptions</b>, which is a list of the perspective options * to use</li> * <li><b>A number of boolean flags:</b> * <ul> * <li><b>NULL</b>, a way to set "no flag"</li> * <li><b>TOTAL</b>, set if the "total data item" (that contains all respondents) * should be displayed</li> * <li><b>SECONDARY</b>, set if data from the secondary dataset should be displayed</li> * <li><b>MEAN</b>, set if a mean chart should be used, otherwise a standard * bar chart is used.</li> * </ul> * </li> * </ul> * * <p>Use {@link EmbedDefinition} to add additional information about how * embedded charts should be displayed.</p> */ public class QueryDefinition { //TODO modify JUnit tests to allow us to change protected/default fields to private /** * The Enum QueryFlag. */ public enum QueryFlag{ /** A way to set "no flag". */ NULL(0), /** * Set if the "total data item" (that contains all respondents) should * be displayed. */ TOTAL(1), /** Set if data from the secondary dataset should be displayed. */ SECONDARY(2), /** * Set if a mean chart should be used, otherwise a standard bar chart is * used. */ MEAN(4); /** * The value of the bit-position used when encoding this flag in a long. */ private final long bitValue; /** * Instantiates a new query flag. * * @param bitValue * the value of the bit-position used when encoding this flag in a long */ QueryFlag(int bitValue) { this.bitValue = bitValue; } /** * Get an array of flags from a long containing a flag-bit-pattern * generated by {@link #encodeFlags(EmbedFlag[])}. * * @param flaglong * a long defining the flags * @return an array of the defined flags */ protected static QueryFlag[] decodeFlags(long flaglong){ if(flaglong == 0) return new QueryFlag[0]; ArrayList<QueryFlag> flags = new ArrayList<>(); for(QueryFlag f: QueryFlag.values()){ if((flaglong & f.bitValue) != 0) flags.add(f); } return flags.toArray(new QueryFlag[flags.size()]); } /** * Encode an array of flags as a long, using a flag-bit-pattern, * that can be decoded by {@link #decodeFlags(long)}. * * @param flags * the flags * @return a long representing the flags */ protected static long encodeFlags(QueryFlag[] flags) { long flaglong = 0; for(QueryFlag f : flags){ flaglong = flaglong | f.bitValue; } return flaglong; } } private String perspectiveID, questionID; private List<Integer> usedPerspectiveOptions; private QueryFlag[] flags; private QuestionMetadata questionMetadata; /** * Instantiates a new query definition from all the individual * definition-components, using a list for the flags. * * @param questionMetadata * the question metadata, used to look up localized texts * for the question and perspective * @param perspectiveID * the perspective's questionID * @param questionID * the question's questionID * @param usedPerspectiveOptions * the used perspective options * @param flags * the flags */ public QueryDefinition(QuestionMetadata questionMetadata, String perspectiveID, String questionID, List<Integer> usedPerspectiveOptions, List<QueryFlag> flags) throws IllegalArgumentException { this.perspectiveID = perspectiveID; this.questionID = questionID; this.usedPerspectiveOptions = usedPerspectiveOptions; this.questionMetadata = questionMetadata; this.flags = flags.toArray(new QueryFlag[flags.size()]); if (questionID == null || !questionMetadata.hasQuestion(questionID)) { throw new IllegalArgumentException("Illegal questionID: " + questionID); } if (perspectiveID == null || !questionMetadata.hasQuestion(perspectiveID)) { throw new IllegalArgumentException("Illegal perspectiveID: " + perspectiveID); } int optionCount = questionMetadata.getOptionCount(perspectiveID); for (int i : usedPerspectiveOptions) { if (i>optionCount) { throw new IllegalArgumentException("Illegal perspective option: " + i + " > " + optionCount); } } } /** * Instantiates a new query definition from a query definition string. * * @param questionMetadata * the question metadata, used to look up localized texts * for the question and perspective * @param restoreString * a query encoded in a string by {@link #getAsString()} * @throws IllegalArgumentException * thrown if the restore string is invalid */ public QueryDefinition(QuestionMetadata questionMetadata, String restoreString) throws IllegalArgumentException { if(restoreString == null || restoreString.isEmpty()) {throw new IllegalArgumentException("No string to restore from");} this.questionMetadata = questionMetadata; LinkedHashMap<String, String> tokens = SharedTools.parseTokens(Base64.decodeString(restoreString)); usedPerspectiveOptions = new LinkedList<>(); flags = new QueryFlag[0]; Collection<String> keys = tokens.keySet(); for (String k : keys) { if (k.equals("q")) { questionID = tokens.get(k); } else if (k.equals("p")) { perspectiveID = tokens.get(k); } else if (k.equals("o")) { String[] sels = tokens.get(k).split(","); for (String s : sels) { try { usedPerspectiveOptions.add(Integer.parseInt(s)); } catch (NumberFormatException e) { throw new IllegalArgumentException(e); } } } else if (k.equals("f")) { try { Integer flag = Integer.parseInt(tokens.get(k)); flags = QueryFlag.decodeFlags(flag); } catch (NumberFormatException e) { throw new IllegalArgumentException(e); } } } if (questionID == null || !questionMetadata.hasQuestion(questionID)) { throw new IllegalArgumentException("Illegal questionID: " + questionID); } if (perspectiveID == null || !questionMetadata.hasQuestion(perspectiveID)) { throw new IllegalArgumentException("Illegal perspectiveID: " + perspectiveID); } int optionCount = questionMetadata.getOptionCount(perspectiveID); for (int i : usedPerspectiveOptions) { if (i>optionCount) { throw new IllegalArgumentException("Illegal perspective option: " + i + " > " + optionCount); } } } /** * Generate a serialized version of the query * @return The query as a string */ private String serialize() { ArrayList<String> out = new ArrayList<>(); if (questionID != null && !questionID.equals("")) { out.add("q=" + questionID); } if (perspectiveID != null && !perspectiveID.equals("")) { out.add("p=" + perspectiveID); if (usedPerspectiveOptions.size() > 0) { List<Integer> tempUsedPerspectiveOptions = new LinkedList<>(); for (int i = 0; i < usedPerspectiveOptions.size(); i++) { tempUsedPerspectiveOptions.add(usedPerspectiveOptions.get(i)); } out.add("o=" + SharedTools.join(tempUsedPerspectiveOptions, ",")); } } if(flags.length > 0){ out.add("f=" + QueryFlag.encodeFlags(flags)); } return SharedTools.join(out, "&"); } /** * Get the query definition as a Base64 encoded string that can be * sent put in the URL or sent between client and server. * * <p>Use in the {@link #QueryDefinition(QuestionMetadata, String)} * constructor to create a new identical query definition instance.</p> * * @return a string representation of the query */ public String getAsString() { return Base64.encodeString(serialize()); } /** * Get the query definition as a human-readable string, * including the encoded version. * * @return The query definition as a human-readable string */ public String getAsHumanString(String prefix) { String query = serialize(); return query + " (" + prefix + "#" + getAsString() + ")"; } /** * Get the perspective's questionID. * * @return the perspective's ID */ public String getPerspectiveID() { return perspectiveID; } /** * Get the question's questionID. * * @return the question's ID */ public String getQuestionID() { return questionID; } /** * Get a list of the used perspective options. * * @return the perspective options */ public List<Integer> getUsedPerspectiveOptions() { return usedPerspectiveOptions; } /** * Checks if a specific flag is set. * * @param flag * the flag * @return true, if the flag is set */ public boolean hasFlag(QueryFlag flag){ for(QueryFlag f: flags){ if(f.equals(flag)) return true; } return false; } /* * Get question specific information: */ /** * Get a long text that describes the question. * * @return the full text */ public String getQuestionFullText() { return questionMetadata.getFullText(questionID); } /** * Get a short text that describes the question. * * @return the short text */ public String getQuestionShortText() { return questionMetadata.getShortText(questionID); } /** * Get the question option count. * * @return the question option count */ public int getQuestionOptionCount() { return questionMetadata.getOptionCount(questionID); } /** * Get the question option texts. * * @return the question option texts */ public List<String> getQuestionOptionTexts() { List<String> texts = new LinkedList<>(); for (String s : questionMetadata.getOptionTexts(questionID)) { texts.add(s); } return texts; } /* * Get perspective specific information: */ /** * Get a short text that describes the perspective. * * @return the perspective short text */ public String getPerspectiveShortText() { return questionMetadata.getShortText(perspectiveID); } /** * Get a long text that describes the question. * * @return the full text */ public String getPerspectiveFullText() { return questionMetadata.getFullText(perspectiveID); } /** * Get the perspective option texts. * * @return the perspective option texts */ public List<String> getPerspectiveOptionTexts() { List<String> texts = new LinkedList<>(); for (String s : questionMetadata.getOptionTexts(perspectiveID)) { texts.add(s); } return texts; } /** * Get the perspective option count. * * @return the perspective option count */ public int getPerspectiveOptionCount() { return questionMetadata.getOptionCount(perspectiveID); } /* * Get question/perspective specific information: */ /** * Check if this question-perspective combination supports displaying * mean data. * * @return true, if averaging is possible */ public boolean hasMean() { return questionMetadata.hasMean(questionID) && questionMetadata.hasMean(perspectiveID); } /** * Check if this question-perspective combination supports displaying * secondary data. * * @return true, if there is secondary data */ public boolean hasSecondary() { return questionMetadata.hasSecondary(questionID) && questionMetadata.hasSecondary(perspectiveID); } }