/* * Copyright (c) 2017 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * 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 org.obiba.magma.datasource.limesurvey; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedSet; import javax.annotation.Nullable; import javax.validation.constraints.NotNull; import org.obiba.magma.AbstractVariableValueSource; import org.obiba.magma.Attribute; import org.obiba.magma.AttributeAwareBuilder; import org.obiba.magma.Category; import org.obiba.magma.NoSuchValueSetException; import org.obiba.magma.Timestamps; import org.obiba.magma.Value; import org.obiba.magma.ValueSet; import org.obiba.magma.ValueType; import org.obiba.magma.Variable; import org.obiba.magma.Variable.Builder; import org.obiba.magma.VariableEntity; import org.obiba.magma.VariableValueSource; import org.obiba.magma.VectorSource; import org.obiba.magma.support.AbstractValueTable; import org.obiba.magma.type.DateTimeType; import org.obiba.magma.type.IntegerType; import org.obiba.magma.type.TextType; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.rowset.SqlRowSet; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @SuppressWarnings("OverlyCoupledClass") class LimesurveyValueTable extends AbstractValueTable { public static final String PARTICIPANT = "Participant"; private final Integer sid; private Map<Integer, LimeQuestion> mapQuestions; private Map<Integer, List<LimeAnswer>> mapAnswers; private Map<Integer, LimeAttributes> mapAttributes; private Set<String> names; private LimesurveyParsingException exception; private final LimesurveyElementProvider elementProvider; LimesurveyValueTable(LimesurveyDatasource datasource, String name, Integer sid) { super(datasource, name); this.sid = sid; elementProvider = new LimesurveyElementProviderJdbc(datasource, sid); setVariableEntityProvider(new LimesurveyVariableEntityProvider(PARTICIPANT, datasource, sid)); } @Override public void initialise() { super.initialise(); names = Sets.newHashSet(); exception = new LimesurveyParsingException("Limesurvey Root Exception", "parentLimeException"); initialiseVariableValueSources(); getVariableEntityProvider().initialise(); if(!exception.getChildren().isEmpty()) { throw exception; } } @NotNull @Override protected LimesurveyVariableEntityProvider getVariableEntityProvider() { return (LimesurveyVariableEntityProvider) super.getVariableEntityProvider(); } String quoteAndPrefix(String identifier) { return getDatasource().quoteAndPrefix(identifier); } private void initialiseVariableValueSources() { clearSources(); mapQuestions = elementProvider.queryQuestions(); mapAnswers = elementProvider.queryExplicitAnswers(); buildImplicitAnswers(); mapAttributes = elementProvider.queryAttributes(); buildVariables(); } private void buildImplicitAnswers() { for(Integer qid : mapQuestions.keySet()) { LimeQuestion question = mapQuestions.get(qid); LimesurveyType type = question.getLimesurveyType(); if(type == null) { throw new LimesurveyParsingException("Unknown type for Limesurvey question: " + question.getName(), "LimeUnknownQuestionType", question.getName()); } List<LimeAnswer> answers = Lists.newArrayList(); if(type.hasImplicitCategories()) { for(String implicitAnswer : type.getImplicitAnswers()) { LimeAnswer answer = LimeAnswer.create(implicitAnswer); answers.add(answer); } mapAnswers.put(qid, answers); } } } private void buildVariables() { buildAdministrativeVariables(); for(LimeQuestion question : mapQuestions.values()) { buildVariableFromQuestion(question); } } private void buildVariableFromQuestion(LimeQuestion question) { if(buildRanking(question)) return; LimeQuestion parentQuestion = null; boolean isHierarchicalQuestion = false; // here are managed special case if(question.hasParentId()) { parentQuestion = getParentQuestion(question); isHierarchicalQuestion = buildArraySubQuestions(question, parentQuestion) || buildArrayDualScale(question, parentQuestion) || buildArrayFlexibleLabels(question, parentQuestion); } buildFileCountIfNecessary(question); buildOtherVariableIfNecessary(question); buildCommentVariableIfNecessary(question, parentQuestion); if(!isHierarchicalQuestion) { // Questions that have sub questions must be ignored (See Builder builder = buildVariable(question); if(builder != null) { buildCategories(question, parentQuestion, builder); } } } private void buildCategories(LimeQuestion question, @Nullable LimeQuestion parentQuestion, Builder builder) { buildLabelAttributes(question, builder); if(question.hasParentId() && parentQuestion != null) { buildCategoriesForVariable(builder, mapAnswers.get(parentQuestion.getQid())); } else if(!hasSubQuestions(question)) { buildCategoriesForVariable(builder, mapAnswers.get(question.getQid())); } String subQuestionFieldTitle = question.hasParentId() ? question.getName() : ""; VariableValueSource variable = new LimesurveyQuestionVariableValueSource(builder, question, subQuestionFieldTitle); addLimesurveyVariableValueSource(variable); } @SuppressWarnings("ReuseOfLocalVariable") private void buildAdministrativeVariables() { Builder vb = Builder.newVariable("startdate", DateTimeType.get(), PARTICIPANT); addLimesurveyVariableValueSource(new LimesurveyVariableValueSource(vb)); vb = Builder.newVariable("submitdate", DateTimeType.get(), PARTICIPANT); addLimesurveyVariableValueSource(new LimesurveyVariableValueSource(vb)); vb = Builder.newVariable("startlanguage", TextType.get(), PARTICIPANT); addLimesurveyVariableValueSource(new LimesurveyVariableValueSource(vb)); vb = Builder.newVariable("lastpage", IntegerType.get(), PARTICIPANT); addLimesurveyVariableValueSource(new LimesurveyVariableValueSource(vb)); } private void buildFileCountIfNecessary(LimeQuestion question) { if(question.getLimesurveyType() == LimesurveyType.FILE_UPLOAD) { Variable.Builder fileCountVb = Variable.Builder .newVariable(question.getName() + " [filecount]", IntegerType.get(), PARTICIPANT); VariableValueSource fileCount = new LimesurveyQuestionVariableValueSource(fileCountVb, question, "_filecount"); addLimesurveyVariableValueSource(fileCount); } } private void buildLabelAttributes(LimeLocalizableEntity localizable, AttributeAwareBuilder<?> builder) { applyImplicitLabel(localizable, builder); for(Attribute attr : localizable.getMagmaAttributes(localizable instanceof LimeQuestion)) { builder.addAttribute(attr); } } private void applyImplicitLabel(LimeLocalizableEntity localizable, AttributeAwareBuilder<?> builder) { LimeAttributes lla = localizable.getImplicitLabel().get(localizable.getName()); if(lla != null) { for(Attribute attr : lla.toMagmaAttributes(localizable instanceof LimeQuestion)) { builder.addAttribute(attr); } } } private boolean buildRanking(LimeQuestion question) { if(question.getLimesurveyType() == LimesurveyType.RANKING) { List<LimeAnswer> answers = mapAnswers.get(question.getQid()); for(int nbChoices = 1; nbChoices < answers.size() + 1; nbChoices++) { Variable.Builder vb = build(question, question.getName() + " [" + nbChoices + "]"); VariableValueSource variable = new LimesurveyQuestionVariableValueSource(vb, question, nbChoices + ""); addLimesurveyVariableValueSource(variable); } return true; } return false; } private boolean buildArrayFlexibleLabels(LimeQuestion question, @Nullable LimeQuestion parentQuestion) { if(parentQuestion != null && parentQuestion.getLimesurveyType() == LimesurveyType.ARRAY_FLEXIBLE_LABELS) { String hierarchicalVariableName = parentQuestion.getName() + " [" + question.getName() + "]"; Variable.Builder vb = build(question, hierarchicalVariableName); buildLabelAttributes(question, vb); List<LimeAnswer> answers = mapAnswers.get(parentQuestion.getQid()); for(LimeAnswer answer : answers) { Category.Builder cb = Category.Builder.newCategory(answer.getName()); buildLabelAttributes(answer, cb); vb.addCategory(cb.build()); } VariableValueSource variable = new LimesurveyQuestionVariableValueSource(vb, question, question.getName()); addLimesurveyVariableValueSource(variable); return true; } return false; } private boolean buildArrayDualScale(LimeQuestion question, @Nullable LimeQuestion parentQuestion) { if(parentQuestion != null && parentQuestion.getLimesurveyType() == LimesurveyType.ARRAY_DUAL_SCALE) { for(int scale = 0; scale < 2; scale++) { String hierarchicalVariableName = parentQuestion.getName() + " [" + question.getName() + "][" + scale + "]"; Variable.Builder vb = build(question, hierarchicalVariableName); buildLabelAttributes(question, vb); List<LimeAnswer> answers = mapAnswers.get(parentQuestion.getQid()); for(LimeAnswer answer : answers) { if(scale == answer.getScaleId()) { Category.Builder cb = Category.Builder.newCategory(answer.getName()); buildLabelAttributes(answer, cb); vb.addCategory(cb.build()); } } VariableValueSource variable = new LimesurveyQuestionVariableValueSource(vb, question, question.getName() + "#" + scale); addLimesurveyVariableValueSource(variable); } return true; } return false; } private void addLimesurveyVariableValueSource(VariableValueSource vvs) { String variableName = vvs.getVariable().getName(); if(!names.add(variableName)) { exception.addChild( new LimesurveyParsingException("'" + getName() + "' contains duplicated variable names: " + variableName, "LimeDuplicateVariableName", getName(), variableName)); } addVariableValueSource(vvs); } @Nullable private Variable.Builder buildVariable(LimeQuestion question) { Variable.Builder builder; // do not create variable for parent question if(!hasSubQuestions(question)) { String variableName = question.getName(); if(question.hasParentId()) { LimeQuestion parentQuestion = getParentQuestion(question); String hierarchicalVariableName = parentQuestion.getName() + " [" + variableName + "]"; builder = build(parentQuestion, hierarchicalVariableName); } else { builder = build(question, variableName); } return builder; } // question has subquestion then return null return null; } private Builder build(LimeQuestion question, String variableName) { Builder builder = Builder.newVariable(variableName, question.getLimesurveyType().getType(), PARTICIPANT); LimeAttributes limeAttributes = mapAttributes.get(question.getQid()); if(limeAttributes != null) { builder.addAttributes(limeAttributes.toMagmaAttributes(true)); } return builder; } private boolean buildArraySubQuestions(LimeQuestion question, @Nullable LimeQuestion parentQuestion) { List<LimeQuestion> scalableSubQuestions = getScaledOneSubQuestions(parentQuestion); if(scalableSubQuestions.isEmpty()) return false; if(!question.isScaleEqual1()) { for(LimeQuestion scalableQuestion : scalableSubQuestions) { String dualName = question.getName() + "_" + scalableQuestion.getName(); String arrayVariableName = parentQuestion.getName() + " [" + dualName + "]"; Variable.Builder subVb = build(parentQuestion, arrayVariableName); buildLabelAttributes(scalableQuestion, subVb); VariableValueSource variable = new LimesurveyQuestionVariableValueSource(subVb, scalableQuestion, dualName); addLimesurveyVariableValueSource(variable); } } return true; } private void buildOtherVariableIfNecessary(LimeQuestion question) { if(question.isUseOther()) { Builder other = build(question, question.getName() + " [other]"); buildSpecialLabel(question, other, "other"); addLimesurveyVariableValueSource(new LimesurveyQuestionVariableValueSource(other, question, "other")); } } /** * Special label are "other" or "comment" * * @param question * @param builder * @param specialLabel */ private void buildSpecialLabel(LimeQuestion question, Builder builder, String specialLabel) { for(Attribute attr : question.getImplicitLabel().get(specialLabel).toMagmaAttributes(true)) { builder.addAttribute(attr); } } private void buildCommentVariableIfNecessary(LimeQuestion question, @Nullable LimeQuestion parentQuestion) { if(question.getLimesurveyType().isCommentable() && !hasSubQuestions(question)) { Builder comment = build(question, question.getName() + " [comment]"); buildSpecialLabel(question, comment, "comment"); addLimesurveyVariableValueSource(new LimesurveyQuestionVariableValueSource(comment, question, "comment")); } else if(parentQuestion != null && parentQuestion.getLimesurveyType().isCommentable()) { String hierarchicalVariableName = parentQuestion.getName() + " [" + question.getName() + "comment]"; Builder comment = build(question, hierarchicalVariableName); buildSpecialLabel(question, comment, "comment"); addLimesurveyVariableValueSource( new LimesurveyQuestionVariableValueSource(comment, question, question.getName() + "comment")); } } private void buildCategoriesForVariable(Builder vb, Iterable<LimeAnswer> limeAnswers) { for(LimeAnswer answer : limeAnswers) { Category.Builder cb = Category.Builder.newCategory(answer.getName()); buildLabelAttributes(answer, cb); vb.addCategory(cb.build()); } } @Nullable private LimeQuestion getParentQuestion(LimeQuestion limeQuestion) { return limeQuestion.hasParentId() ? mapQuestions.get(limeQuestion.getParentQid()) : null; } private boolean hasSubQuestions(final LimeQuestion limeQuestion) { return Iterables.any(mapQuestions.values(), new Predicate<LimeQuestion>() { @Override public boolean apply(LimeQuestion question) { return question.getParentQid() == limeQuestion.getQid(); } }); } private List<LimeQuestion> getScaledOneSubQuestions(@Nullable final LimeQuestion limeQuestion) { return Lists.newArrayList(Iterables.filter(mapQuestions.values(), new Predicate<LimeQuestion>() { @Override public boolean apply(LimeQuestion question) { return question.getParentQid() == limeQuestion.getQid() && question.isScaleEqual1(); } })); } @NotNull @Override public LimesurveyDatasource getDatasource() { return (LimesurveyDatasource) super.getDatasource(); } public Integer getSid() { return sid; } @Override public ValueSet getValueSet(VariableEntity entity) throws NoSuchValueSetException { return new LimesurveyValueSet(this, entity); } @Override public Timestamps getValueSetTimestamps(VariableEntity entity) throws NoSuchValueSetException { return new LimesurveyValueSet(this, entity).getTimestamps(); } @NotNull @Override public Timestamps getTimestamps() { return new LimesurveyTimestamps(this); } class LimesurveyVariableValueSource extends AbstractVariableValueSource implements VariableValueSource, VectorSource { private Variable variable; LimesurveyVariableValueSource(Builder vb) { setVariable(vb.build()); } protected void setVariable(Variable variable) { this.variable = variable; } @NotNull @Override public ValueType getValueType() { return variable.getValueType(); } @NotNull @Override public Value getValue(ValueSet valueSet) { LimesurveyValueSet limesurveyValueSet = (LimesurveyValueSet) valueSet; return limesurveyValueSet.getValue(getVariable().getValueType(), getLimesurveyVariableField()); } @Override public boolean supportVectorSource() { return true; } @NotNull @Override public VectorSource asVectorSource() { return this; } @NotNull @Override public Variable getVariable() { return variable; } public String getLimesurveyVariableField() { return variable.getName(); } @Override //TODO move into provider implementation public Iterable<Value> getValues(final SortedSet<VariableEntity> entities) { return new Iterable<Value>() { @Override public Iterator<Value> iterator() { NamedParameterJdbcOperations jdbcTemplate = new NamedParameterJdbcTemplate(getDatasource().getDataSource()); MapSqlParameterSource parameters = new MapSqlParameterSource(); parameters.addValue("ids", Lists.newArrayList(extractIdentifiers(entities))); String sql = "SELECT " + quoteAndPrefix(getLimesurveyVariableField()) + " FROM " + quoteAndPrefix("survey_" + getSid()) + " WHERE token IN (:ids) ORDER BY token"; return new ValueIterator(entities, jdbcTemplate.queryForRowSet(sql, parameters)); } private Iterable<String> extractIdentifiers(Iterable<VariableEntity> entities) { return Iterables.transform(entities, new Function<VariableEntity, String>() { @Override public String apply(VariableEntity input) { return input.getIdentifier(); } }); } }; } private class ValueIterator implements Iterator<Value> { private final Iterator<VariableEntity> idsIterator; private SqlRowSet rows; private ValueIterator(Iterable<VariableEntity> entities, SqlRowSet rows) { idsIterator = entities.iterator(); if(!Iterables.isEmpty(entities)) { this.rows = rows; } } @Override public boolean hasNext() { return idsIterator.hasNext(); } @Override public Value next() { if(!hasNext()) { throw new NoSuchElementException(); } idsIterator.next(); rows.next(); Object object = rows.getObject(getLimesurveyVariableField()); return variable.getValueType().valueOf("".equals(object) ? null : object); } @Override public void remove() { throw new UnsupportedOperationException(); } } } class LimesurveyQuestionVariableValueSource extends LimesurveyVariableValueSource { private final LimeQuestion question; private String subQuestionFieldTitle = ""; LimesurveyQuestionVariableValueSource(Builder vb, LimeQuestion question, String subQuestionFieldTitle) { super(vb); this.question = question; this.subQuestionFieldTitle = subQuestionFieldTitle; vb.addAttribute(Attribute.Builder.newAttribute("SGQA").withNamespace(LimeAttributes.LIMESURVEY_NAMESPACE) .withValue(getLimesurveyVariableField()).build()); vb.addAttribute( Attribute.Builder.newAttribute("SGQ").withNamespace(LimeAttributes.LIMESURVEY_NAMESPACE).withValue(getSgqId()) .build()); setVariable(vb.build()); } // SGQA identifier // see http://docs.limesurvey.org/tiki-index.php?page=SGQA+identifier&structure=English+Instructions+for+LimeSurvey @Override public String getLimesurveyVariableField() { int qId = question.hasParentId() ? question.getParentQid() : question.getQid(); return sid + "X" + question.getGroupId() + "X" + qId + subQuestionFieldTitle; } // SGQ public String getSgqId() { int qId = question.hasParentId() ? question.getParentQid() : question.getQid(); return sid + "X" + question.getGroupId() + "X" + qId; } } }