SEBSERV-75 implementation
This commit is contained in:
		
							parent
							
								
									8ab3ddf725
								
							
						
					
					
						commit
						e5f5bc5c02
					
				
					 25 changed files with 1184 additions and 112 deletions
				
			
		| 
						 | 
					@ -38,6 +38,8 @@ public final class Constants {
 | 
				
			||||||
    public static final Character CARRIAGE_RETURN = '\n';
 | 
					    public static final Character CARRIAGE_RETURN = '\n';
 | 
				
			||||||
    public static final Character CURLY_BRACE_OPEN = '{';
 | 
					    public static final Character CURLY_BRACE_OPEN = '{';
 | 
				
			||||||
    public static final Character CURLY_BRACE_CLOSE = '}';
 | 
					    public static final Character CURLY_BRACE_CLOSE = '}';
 | 
				
			||||||
 | 
					    public static final Character SQUARE_BRACE_OPEN = '[';
 | 
				
			||||||
 | 
					    public static final Character SQUARE_BRACE_CLOSE = ']';
 | 
				
			||||||
    public static final Character COLON = ':';
 | 
					    public static final Character COLON = ':';
 | 
				
			||||||
    public static final Character SEMICOLON = ';';
 | 
					    public static final Character SEMICOLON = ';';
 | 
				
			||||||
    public static final Character PERCENTAGE = '%';
 | 
					    public static final Character PERCENTAGE = '%';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,7 +75,6 @@ public final class CircuitBreaker<T> {
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param asyncRunner the AsyncRunner used to create asynchronous calls on the given supplier function */
 | 
					     * @param asyncRunner the AsyncRunner used to create asynchronous calls on the given supplier function */
 | 
				
			||||||
    CircuitBreaker(final AsyncRunner asyncRunner) {
 | 
					    CircuitBreaker(final AsyncRunner asyncRunner) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
        this(
 | 
					        this(
 | 
				
			||||||
                asyncRunner,
 | 
					                asyncRunner,
 | 
				
			||||||
                DEFAULT_MAX_FAILING_ATTEMPTS,
 | 
					                DEFAULT_MAX_FAILING_ATTEMPTS,
 | 
				
			||||||
| 
						 | 
					@ -174,7 +173,9 @@ public final class CircuitBreaker<T> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                this.state = State.HALF_OPEN;
 | 
					                this.state = State.HALF_OPEN;
 | 
				
			||||||
                this.failingCount.set(0);
 | 
					                this.failingCount.set(0);
 | 
				
			||||||
                return Result.ofError(OPEN_STATE_EXCEPTION);
 | 
					                return Result.ofError(new RuntimeException(
 | 
				
			||||||
 | 
					                        "Set CircuitBraker to half-open state. Cause: ",
 | 
				
			||||||
 | 
					                        result.getError()));
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                // try again
 | 
					                // try again
 | 
				
			||||||
                return protectedRun(supplier);
 | 
					                return protectedRun(supplier);
 | 
				
			||||||
| 
						 | 
					@ -202,7 +203,9 @@ public final class CircuitBreaker<T> {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.state = State.OPEN;
 | 
					            this.state = State.OPEN;
 | 
				
			||||||
            return Result.ofError(OPEN_STATE_EXCEPTION);
 | 
					            return Result.ofError(new RuntimeException(
 | 
				
			||||||
 | 
					                    "Set CircuitBraker to open state. Cause: ",
 | 
				
			||||||
 | 
					                    result.getError()));
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // on success go to CLODED state
 | 
					            // on success go to CLODED state
 | 
				
			||||||
            if (log.isDebugEnabled()) {
 | 
					            if (log.isDebugEnabled()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,8 @@ public final class LmsSetup implements GrantEntity, Activatable {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public enum LmsType {
 | 
					    public enum LmsType {
 | 
				
			||||||
        MOCKUP(Features.COURSE_API),
 | 
					        MOCKUP(Features.COURSE_API),
 | 
				
			||||||
        OPEN_EDX(Features.COURSE_API, Features.SEB_RESTICTION);
 | 
					        OPEN_EDX(Features.COURSE_API, Features.SEB_RESTICTION),
 | 
				
			||||||
 | 
					        MOODLE(Features.COURSE_API);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public final EnumSet<Features> features;
 | 
					        public final EnumSet<Features> features;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -269,10 +269,30 @@ public final class Utils {
 | 
				
			||||||
        return dateTime.withZone(DateTimeZone.UTC);
 | 
					        return dateTime.withZone(DateTimeZone.UTC);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static DateTime toDateTimeUTC(final Long timestamp) {
 | 
				
			||||||
 | 
					        if (timestamp == null) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return toDateTimeUTC(timestamp.longValue());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static DateTime toDateTimeUTC(final long timestamp) {
 | 
					    public static DateTime toDateTimeUTC(final long timestamp) {
 | 
				
			||||||
        return new DateTime(timestamp, DateTimeZone.UTC);
 | 
					        return new DateTime(timestamp, DateTimeZone.UTC);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static DateTime toDateTimeUTCUnix(final Long timestamp) {
 | 
				
			||||||
 | 
					        if (timestamp == null || timestamp.longValue() <= 0) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return toDateTimeUTCUnix(timestamp.longValue());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static DateTime toDateTimeUTCUnix(final long timestamp) {
 | 
				
			||||||
 | 
					        return new DateTime(timestamp * 1000, DateTimeZone.UTC);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static Long toTimestamp(final String dateString) {
 | 
					    public static Long toTimestamp(final String dateString) {
 | 
				
			||||||
        if (StringUtils.isBlank(dateString)) {
 | 
					        if (StringUtils.isBlank(dateString)) {
 | 
				
			||||||
            return null;
 | 
					            return null;
 | 
				
			||||||
| 
						 | 
					@ -547,4 +567,48 @@ public final class Utils {
 | 
				
			||||||
            return StringUtils.EMPTY;
 | 
					            return StringUtils.EMPTY;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static String toAppFormUrlEncodedBody(final MultiValueMap<String, String> attributes) {
 | 
				
			||||||
 | 
					        return attributes
 | 
				
			||||||
 | 
					                .entrySet()
 | 
				
			||||||
 | 
					                .stream()
 | 
				
			||||||
 | 
					                .reduce(
 | 
				
			||||||
 | 
					                        new StringBuilder(),
 | 
				
			||||||
 | 
					                        (sb, entry) -> {
 | 
				
			||||||
 | 
					                            final String name = entry.getKey();
 | 
				
			||||||
 | 
					                            final List<String> values = entry.getValue();
 | 
				
			||||||
 | 
					                            if (values == null || values.isEmpty()) {
 | 
				
			||||||
 | 
					                                return sb;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            if (sb.length() > 0) {
 | 
				
			||||||
 | 
					                                sb.append(Constants.AMPERSAND);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            if (sb.length() == 1) {
 | 
				
			||||||
 | 
					                                return sb.append(name).append(Constants.EQUALITY_SIGN).append(values.get(0));
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            return sb.append(toAppFormUrlEncodedBody(name, values));
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        (sb1, sb2) -> sb1.append(sb2))
 | 
				
			||||||
 | 
					                .toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static final String toAppFormUrlEncodedBody(final String name, final Collection<String> array) {
 | 
				
			||||||
 | 
					        final String _name = name.contains(String.valueOf(Constants.SQUARE_BRACE_OPEN))
 | 
				
			||||||
 | 
					                ? name
 | 
				
			||||||
 | 
					                : name + Constants.SQUARE_BRACE_OPEN + Constants.SQUARE_BRACE_CLOSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return array
 | 
				
			||||||
 | 
					                .stream()
 | 
				
			||||||
 | 
					                .reduce(
 | 
				
			||||||
 | 
					                        new StringBuilder(),
 | 
				
			||||||
 | 
					                        (sb, entry) -> {
 | 
				
			||||||
 | 
					                            if (sb.length() > 0) {
 | 
				
			||||||
 | 
					                                sb.append(Constants.AMPERSAND);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            return sb.append(_name).append(Constants.EQUALITY_SIGN).append(entry);
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        (sb1, sb2) -> sb1.append(sb2))
 | 
				
			||||||
 | 
					                .toString();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -312,7 +312,7 @@ public class ExamForm implements TemplateComposer {
 | 
				
			||||||
                        QuizData.QUIZ_ATTR_DESCRIPTION,
 | 
					                        QuizData.QUIZ_ATTR_DESCRIPTION,
 | 
				
			||||||
                        FORM_DESCRIPTION_TEXT_KEY,
 | 
					                        FORM_DESCRIPTION_TEXT_KEY,
 | 
				
			||||||
                        exam.description)
 | 
					                        exam.description)
 | 
				
			||||||
                        .asArea()
 | 
					                        .asHTML()
 | 
				
			||||||
                        .readonly(true)
 | 
					                        .readonly(true)
 | 
				
			||||||
                        .withInputSpan(6)
 | 
					                        .withInputSpan(6)
 | 
				
			||||||
                        .withEmptyCellSeparation(false))
 | 
					                        .withEmptyCellSeparation(false))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -257,9 +257,10 @@ public class QuizDiscoveryList implements TemplateComposer {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        action.getSingleSelection();
 | 
					        action.getSingleSelection();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final ModalInputDialog<Void> dialog = new ModalInputDialog<>(
 | 
					        final ModalInputDialog<Void> dialog = new ModalInputDialog<Void>(
 | 
				
			||||||
                action.pageContext().getParent().getShell(),
 | 
					                action.pageContext().getParent().getShell(),
 | 
				
			||||||
                this.widgetFactory);
 | 
					                this.widgetFactory)
 | 
				
			||||||
 | 
					                        .setLargeDialogWidth();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialog.open(
 | 
					        dialog.open(
 | 
				
			||||||
                DETAILS_TITLE_TEXT_KEY,
 | 
					                DETAILS_TITLE_TEXT_KEY,
 | 
				
			||||||
| 
						 | 
					@ -277,7 +278,7 @@ public class QuizDiscoveryList implements TemplateComposer {
 | 
				
			||||||
        final Composite parent = pc.getParent();
 | 
					        final Composite parent = pc.getParent();
 | 
				
			||||||
        final Composite grid = this.widgetFactory.createPopupScrollComposite(parent);
 | 
					        final Composite grid = this.widgetFactory.createPopupScrollComposite(parent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.pageService.formBuilder(pc.copyOf(grid), 3)
 | 
					        final FormBuilder formbuilder = this.pageService.formBuilder(pc.copyOf(grid), 3)
 | 
				
			||||||
                .withEmptyCellSeparation(false)
 | 
					                .withEmptyCellSeparation(false)
 | 
				
			||||||
                .readonly(true)
 | 
					                .readonly(true)
 | 
				
			||||||
                .addFieldIf(
 | 
					                .addFieldIf(
 | 
				
			||||||
| 
						 | 
					@ -299,7 +300,7 @@ public class QuizDiscoveryList implements TemplateComposer {
 | 
				
			||||||
                        QuizData.QUIZ_ATTR_DESCRIPTION,
 | 
					                        QuizData.QUIZ_ATTR_DESCRIPTION,
 | 
				
			||||||
                        QUIZ_DETAILS_DESCRIPTION_TEXT_KEY,
 | 
					                        QUIZ_DETAILS_DESCRIPTION_TEXT_KEY,
 | 
				
			||||||
                        quizData.description)
 | 
					                        quizData.description)
 | 
				
			||||||
                        .asArea())
 | 
					                        .asHTML())
 | 
				
			||||||
                .addField(FormBuilder.text(
 | 
					                .addField(FormBuilder.text(
 | 
				
			||||||
                        QuizData.QUIZ_ATTR_START_TIME,
 | 
					                        QuizData.QUIZ_ATTR_START_TIME,
 | 
				
			||||||
                        QUIZ_DETAILS_STARTTIME_TEXT_KEY,
 | 
					                        QUIZ_DETAILS_STARTTIME_TEXT_KEY,
 | 
				
			||||||
| 
						 | 
					@ -311,8 +312,20 @@ public class QuizDiscoveryList implements TemplateComposer {
 | 
				
			||||||
                .addField(FormBuilder.text(
 | 
					                .addField(FormBuilder.text(
 | 
				
			||||||
                        QuizData.QUIZ_ATTR_START_URL,
 | 
					                        QuizData.QUIZ_ATTR_START_URL,
 | 
				
			||||||
                        QUIZ_DETAILS_URL_TEXT_KEY,
 | 
					                        QUIZ_DETAILS_URL_TEXT_KEY,
 | 
				
			||||||
                        quizData.startURL))
 | 
					                        quizData.startURL));
 | 
				
			||||||
                .build();
 | 
					
 | 
				
			||||||
 | 
					        if (!quizData.additionalAttributes.isEmpty()) {
 | 
				
			||||||
 | 
					            quizData.additionalAttributes
 | 
				
			||||||
 | 
					                    .entrySet()
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .forEach(entry -> formbuilder
 | 
				
			||||||
 | 
					                            .addField(FormBuilder.text(
 | 
				
			||||||
 | 
					                                    entry.getKey(),
 | 
				
			||||||
 | 
					                                    new LocTextKey(entry.getKey()),
 | 
				
			||||||
 | 
					                                    entry.getValue())));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        formbuilder.build();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,7 @@ import java.util.function.Predicate;
 | 
				
			||||||
import org.apache.commons.lang3.BooleanUtils;
 | 
					import org.apache.commons.lang3.BooleanUtils;
 | 
				
			||||||
import org.apache.commons.lang3.StringUtils;
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
import org.eclipse.rap.rwt.RWT;
 | 
					import org.eclipse.rap.rwt.RWT;
 | 
				
			||||||
 | 
					import org.eclipse.swt.browser.Browser;
 | 
				
			||||||
import org.eclipse.swt.graphics.Color;
 | 
					import org.eclipse.swt.graphics.Color;
 | 
				
			||||||
import org.eclipse.swt.widgets.Button;
 | 
					import org.eclipse.swt.widgets.Button;
 | 
				
			||||||
import org.eclipse.swt.widgets.Control;
 | 
					import org.eclipse.swt.widgets.Control;
 | 
				
			||||||
| 
						 | 
					@ -127,6 +128,11 @@ public final class Form implements FormBinding {
 | 
				
			||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Form putReadonlyField(final String name, final Label label, final Browser field) {
 | 
				
			||||||
 | 
					        this.formFields.add(name, createReadonlyAccessor(label, field));
 | 
				
			||||||
 | 
					        return this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Form putField(final String name, final Label label, final Text field, final Label errorLabel) {
 | 
					    Form putField(final String name, final Label label, final Text field, final Label errorLabel) {
 | 
				
			||||||
        this.formFields.add(name, createAccessor(label, field, errorLabel));
 | 
					        this.formFields.add(name, createAccessor(label, field, errorLabel));
 | 
				
			||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
| 
						 | 
					@ -285,6 +291,12 @@ public final class Form implements FormBinding {
 | 
				
			||||||
            @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); }
 | 
					            @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    private FormFieldAccessor createReadonlyAccessor(final Label label, final Browser field) {
 | 
				
			||||||
 | 
					        return new FormFieldAccessor(label, field, null) {
 | 
				
			||||||
 | 
					            @Override public String getStringValue() { return null; }
 | 
				
			||||||
 | 
					            @Override public void setStringValue(final String value) { field.setText( (value == null) ? StringUtils.EMPTY : value); }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    private FormFieldAccessor createAccessor(final Label label, final Text text, final Label errorLabel) {
 | 
					    private FormFieldAccessor createAccessor(final Label label, final Text text, final Label errorLabel) {
 | 
				
			||||||
        return new FormFieldAccessor(label, text, errorLabel) {
 | 
					        return new FormFieldAccessor(label, text, errorLabel) {
 | 
				
			||||||
            @Override public String getStringValue() {return text.getText();}
 | 
					            @Override public String getStringValue() {return text.getText();}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import java.util.function.Consumer;
 | 
				
			||||||
import org.apache.commons.lang3.StringUtils;
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
import org.eclipse.rap.rwt.RWT;
 | 
					import org.eclipse.rap.rwt.RWT;
 | 
				
			||||||
import org.eclipse.swt.SWT;
 | 
					import org.eclipse.swt.SWT;
 | 
				
			||||||
 | 
					import org.eclipse.swt.browser.Browser;
 | 
				
			||||||
import org.eclipse.swt.layout.GridData;
 | 
					import org.eclipse.swt.layout.GridData;
 | 
				
			||||||
import org.eclipse.swt.widgets.Composite;
 | 
					import org.eclipse.swt.widgets.Composite;
 | 
				
			||||||
import org.eclipse.swt.widgets.Label;
 | 
					import org.eclipse.swt.widgets.Label;
 | 
				
			||||||
| 
						 | 
					@ -24,12 +25,17 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public final class TextFieldBuilder extends FieldBuilder<String> {
 | 
					public final class TextFieldBuilder extends FieldBuilder<String> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String HTML_TEXT_BLOCK_START =
 | 
				
			||||||
 | 
					            "<span style=\"font: 12px Arial, Helvetica, sans-serif;color: #4a4a4a;\">";
 | 
				
			||||||
 | 
					    private static final String HTML_TEXT_BLOCK_END = "</span>";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    boolean isPassword = false;
 | 
					    boolean isPassword = false;
 | 
				
			||||||
    boolean isNumber = false;
 | 
					    boolean isNumber = false;
 | 
				
			||||||
    Consumer<String> numberCheck = null;
 | 
					    Consumer<String> numberCheck = null;
 | 
				
			||||||
    boolean isArea = false;
 | 
					    boolean isArea = false;
 | 
				
			||||||
    int areaMinHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
 | 
					    int areaMinHeight = WidgetFactory.TEXT_AREA_INPUT_MIN_HEIGHT;
 | 
				
			||||||
    boolean isColorbox = false;
 | 
					    boolean isColorbox = false;
 | 
				
			||||||
 | 
					    boolean isHTML = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    TextFieldBuilder(final String name, final LocTextKey label, final String value) {
 | 
					    TextFieldBuilder(final String name, final LocTextKey label, final String value) {
 | 
				
			||||||
        super(name, label, value);
 | 
					        super(name, label, value);
 | 
				
			||||||
| 
						 | 
					@ -62,6 +68,11 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
 | 
				
			||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public TextFieldBuilder asHTML() {
 | 
				
			||||||
 | 
					        this.isHTML = true;
 | 
				
			||||||
 | 
					        return this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public TextFieldBuilder asColorbox() {
 | 
					    public TextFieldBuilder asColorbox() {
 | 
				
			||||||
        this.isColorbox = true;
 | 
					        this.isColorbox = true;
 | 
				
			||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
| 
						 | 
					@ -72,6 +83,21 @@ public final class TextFieldBuilder extends FieldBuilder<String> {
 | 
				
			||||||
        final boolean readonly = builder.readonly || this.readonly;
 | 
					        final boolean readonly = builder.readonly || this.readonly;
 | 
				
			||||||
        final Label titleLabel = createTitleLabel(builder.formParent, builder, this);
 | 
					        final Label titleLabel = createTitleLabel(builder.formParent, builder, this);
 | 
				
			||||||
        final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
 | 
					        final Composite fieldGrid = createFieldGrid(builder.formParent, this.spanInput);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (readonly && this.isHTML) {
 | 
				
			||||||
 | 
					            final Browser browser = new Browser(fieldGrid, SWT.NONE);
 | 
				
			||||||
 | 
					            final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, true);
 | 
				
			||||||
 | 
					            gridData.minimumHeight = this.areaMinHeight;
 | 
				
			||||||
 | 
					            browser.setLayoutData(gridData);
 | 
				
			||||||
 | 
					            if (StringUtils.isNoneBlank(this.value)) {
 | 
				
			||||||
 | 
					                browser.setText(HTML_TEXT_BLOCK_START + this.value + HTML_TEXT_BLOCK_END);
 | 
				
			||||||
 | 
					            } else if (readonly) {
 | 
				
			||||||
 | 
					                browser.setText(Constants.EMPTY_NOTE);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            builder.form.putReadonlyField(this.name, titleLabel, browser);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final Text textInput = (this.isNumber)
 | 
					        final Text textInput = (this.isNumber)
 | 
				
			||||||
                ? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly)
 | 
					                ? builder.widgetFactory.numberInput(fieldGrid, this.numberCheck, readonly)
 | 
				
			||||||
                : (this.isArea)
 | 
					                : (this.isArea)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -194,7 +194,7 @@ public interface PageService {
 | 
				
			||||||
     * @param <T> the type of the Entity of the table
 | 
					     * @param <T> the type of the Entity of the table
 | 
				
			||||||
     * @return TableBuilder of specified type */
 | 
					     * @return TableBuilder of specified type */
 | 
				
			||||||
    default <T extends Entity> TableBuilder<T> entityTableBuilder(final RestCall<Page<T>> apiCall) {
 | 
					    default <T extends Entity> TableBuilder<T> entityTableBuilder(final RestCall<Page<T>> apiCall) {
 | 
				
			||||||
        return entityTableBuilder(apiCall.getEntityType().name(), apiCall);
 | 
					        return entityTableBuilder(apiCall.getClass().getSimpleName(), apiCall);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Get an new TableBuilder for specified page based RestCall.
 | 
					    /** Get an new TableBuilder for specified page based RestCall.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -213,7 +213,7 @@ public class EntityTable<ROW extends Entity> {
 | 
				
			||||||
        this.navigator = new TableNavigator(this);
 | 
					        this.navigator = new TableNavigator(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        createTableColumns();
 | 
					        createTableColumns();
 | 
				
			||||||
        initCurrentPageFromUserAttr();
 | 
					        this.pageNumber = initCurrentPageFromUserAttr();
 | 
				
			||||||
        initFilterFromUserAttrs();
 | 
					        initFilterFromUserAttrs();
 | 
				
			||||||
        initSortFromUserAttr();
 | 
					        initSortFromUserAttr();
 | 
				
			||||||
        updateTableRows(
 | 
					        updateTableRows(
 | 
				
			||||||
| 
						 | 
					@ -269,7 +269,7 @@ public class EntityTable<ROW extends Entity> {
 | 
				
			||||||
                this.sortColumn,
 | 
					                this.sortColumn,
 | 
				
			||||||
                this.sortOrder);
 | 
					                this.sortOrder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        updateCurrentPageAttr(pageSelection);
 | 
					        updateCurrentPageAttr();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public void reset() {
 | 
					    public void reset() {
 | 
				
			||||||
| 
						 | 
					@ -282,14 +282,8 @@ public class EntityTable<ROW extends Entity> {
 | 
				
			||||||
    public void applyFilter() {
 | 
					    public void applyFilter() {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            updateTableRows(
 | 
					 | 
				
			||||||
                    this.pageNumber,
 | 
					 | 
				
			||||||
                    this.pageSize,
 | 
					 | 
				
			||||||
                    this.sortColumn,
 | 
					 | 
				
			||||||
                    this.sortOrder);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            updateFilterUserAttrs();
 | 
					            updateFilterUserAttrs();
 | 
				
			||||||
            this.selectPage(0);
 | 
					            this.selectPage(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        } catch (final Exception e) {
 | 
					        } catch (final Exception e) {
 | 
				
			||||||
            log.error("Unexpected error while trying to apply filter: ", e);
 | 
					            log.error("Unexpected error while trying to apply filter: ", e);
 | 
				
			||||||
| 
						 | 
					@ -301,11 +295,13 @@ public class EntityTable<ROW extends Entity> {
 | 
				
			||||||
            this.sortColumn = columnName;
 | 
					            this.sortColumn = columnName;
 | 
				
			||||||
            this.sortOrder = PageSortOrder.ASCENDING;
 | 
					            this.sortOrder = PageSortOrder.ASCENDING;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (columnName != null) {
 | 
				
			||||||
                updateTableRows(
 | 
					                updateTableRows(
 | 
				
			||||||
                        this.pageNumber,
 | 
					                        this.pageNumber,
 | 
				
			||||||
                        this.pageSize,
 | 
					                        this.pageSize,
 | 
				
			||||||
                        this.sortColumn,
 | 
					                        this.sortColumn,
 | 
				
			||||||
                        this.sortOrder);
 | 
					                        this.sortOrder);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            updateSortUserAttr();
 | 
					            updateSortUserAttr();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -632,26 +628,29 @@ public class EntityTable<ROW extends Entity> {
 | 
				
			||||||
        // TODO handle selection tool-tips on cell level
 | 
					        // TODO handle selection tool-tips on cell level
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void updateCurrentPageAttr(final int page) {
 | 
					    private void updateCurrentPageAttr() {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            this.pageService
 | 
					            this.pageService
 | 
				
			||||||
                    .getCurrentUser()
 | 
					                    .getCurrentUser()
 | 
				
			||||||
                    .putAttribute(this.currentPageAttrName, String.valueOf(page));
 | 
					                    .putAttribute(this.currentPageAttrName, String.valueOf(this.pageNumber));
 | 
				
			||||||
        } catch (final Exception e) {
 | 
					        } catch (final Exception e) {
 | 
				
			||||||
            log.error("Failed to put current page attribute to current user attributes", e);
 | 
					            log.error("Failed to put current page attribute to current user attributes", e);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private void initCurrentPageFromUserAttr() {
 | 
					    private int initCurrentPageFromUserAttr() {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            final String currentPage = this.pageService
 | 
					            final String currentPage = this.pageService
 | 
				
			||||||
                    .getCurrentUser()
 | 
					                    .getCurrentUser()
 | 
				
			||||||
                    .getAttribute(this.currentPageAttrName);
 | 
					                    .getAttribute(this.currentPageAttrName);
 | 
				
			||||||
            if (StringUtils.isNotBlank(currentPage)) {
 | 
					            if (StringUtils.isNotBlank(currentPage)) {
 | 
				
			||||||
                this.selectPage(Integer.parseInt(currentPage));
 | 
					                return Integer.parseInt(currentPage);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                return 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (final Exception e) {
 | 
					        } catch (final Exception e) {
 | 
				
			||||||
            log.error("Failed to get sort attribute form current user attributes", e);
 | 
					            log.error("Failed to get sort attribute form current user attributes", e);
 | 
				
			||||||
 | 
					            return 1;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ import org.eclipse.swt.layout.GridLayout;
 | 
				
			||||||
import org.eclipse.swt.widgets.MessageBox;
 | 
					import org.eclipse.swt.widgets.MessageBox;
 | 
				
			||||||
import org.eclipse.swt.widgets.Shell;
 | 
					import org.eclipse.swt.widgets.Shell;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
 | 
					import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public final class Message extends MessageBox {
 | 
					public final class Message extends MessageBox {
 | 
				
			||||||
| 
						 | 
					@ -35,7 +36,13 @@ public final class Message extends MessageBox {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    protected void prepareOpen() {
 | 
					    protected void prepareOpen() {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
            super.prepareOpen();
 | 
					            super.prepareOpen();
 | 
				
			||||||
 | 
					        } catch (final IllegalArgumentException e) {
 | 
				
			||||||
 | 
					            // fallback on markup text error
 | 
				
			||||||
 | 
					            super.setMessage(Utils.escapeHTML_XML_EcmaScript(super.getMessage()));
 | 
				
			||||||
 | 
					            super.prepareOpen();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        final GridLayout layout = (GridLayout) super.shell.getLayout();
 | 
					        final GridLayout layout = (GridLayout) super.shell.getLayout();
 | 
				
			||||||
        layout.marginTop = 10;
 | 
					        layout.marginTop = 10;
 | 
				
			||||||
        layout.marginLeft = 10;
 | 
					        layout.marginLeft = 10;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -149,7 +149,7 @@ public interface LmsAPIService {
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return new Page<>(
 | 
					            return new Page<>(
 | 
				
			||||||
                    (quizzes.size() / pageSize) + 1,
 | 
					                    (quizzes.size() / pageSize),
 | 
				
			||||||
                    pageNumber,
 | 
					                    pageNumber,
 | 
				
			||||||
                    sortAttribute,
 | 
					                    sortAttribute,
 | 
				
			||||||
                    quizzes.subList(start, end));
 | 
					                    quizzes.subList(start, end));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,9 +10,11 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.Arrays;
 | 
					import java.util.Arrays;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					import java.util.Collections;
 | 
				
			||||||
import java.util.HashSet;
 | 
					import java.util.HashSet;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.apache.commons.lang3.StringUtils;
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,7 +70,14 @@ public interface LmsAPITemplate {
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param ids the Set of Quiz identifiers to get the QuizData for
 | 
					     * @param ids the Set of Quiz identifiers to get the QuizData for
 | 
				
			||||||
     * @return Collection of all QuizData from the given id set */
 | 
					     * @return Collection of all QuizData from the given id set */
 | 
				
			||||||
    Collection<Result<QuizData>> getQuizzes(Set<String> ids);
 | 
					    default Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
 | 
				
			||||||
 | 
					        return getQuizzes(new FilterMap())
 | 
				
			||||||
 | 
					                .getOrElse(() -> Collections.emptyList())
 | 
				
			||||||
 | 
					                .stream()
 | 
				
			||||||
 | 
					                .filter(quiz -> ids.contains(quiz.id))
 | 
				
			||||||
 | 
					                .map(quiz -> Result.of(quiz))
 | 
				
			||||||
 | 
					                .collect(Collectors.toList());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Get all QuizData for the set of QuizData identifiers from LMS API in a collection
 | 
					    /** Get all QuizData for the set of QuizData identifiers from LMS API in a collection
 | 
				
			||||||
     * of Result. If particular Quiz cannot be loaded because of errors or deletion,
 | 
					     * of Result. If particular Quiz cannot be loaded because of errors or deletion,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,87 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.NoSuchElementException;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					import java.util.function.Function;
 | 
				
			||||||
 | 
					import java.util.function.Supplier;
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.Constants;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.async.AsyncService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public abstract class CourseAccess {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected final MemoizingCircuitBreaker<List<QuizData>> allQuizzesSupplier;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected CourseAccess(final AsyncService asyncService) {
 | 
				
			||||||
 | 
					        this.allQuizzesSupplier = asyncService.createMemoizingCircuitBreaker(
 | 
				
			||||||
 | 
					                allQuizzesSupplier(),
 | 
				
			||||||
 | 
					                3,
 | 
				
			||||||
 | 
					                Constants.MINUTE_IN_MILLIS,
 | 
				
			||||||
 | 
					                Constants.MINUTE_IN_MILLIS,
 | 
				
			||||||
 | 
					                true,
 | 
				
			||||||
 | 
					                Constants.HOUR_IN_MILLIS);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Result<QuizData> getQuizFromCache(final String id) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					            return this.allQuizzesSupplier
 | 
				
			||||||
 | 
					                    .getChached()
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .filter(qd -> id.equals(qd.id))
 | 
				
			||||||
 | 
					                    .findFirst()
 | 
				
			||||||
 | 
					                    .orElseThrow(() -> new NoSuchElementException("No cached quiz: " + id));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					            final List<QuizData> cached = this.allQuizzesSupplier.getChached();
 | 
				
			||||||
 | 
					            if (cached == null) {
 | 
				
			||||||
 | 
					                throw new RuntimeException("No cached quizzes");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final Map<String, QuizData> cacheMapping = cached
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .collect(Collectors.toMap(q -> q.id, Function.identity()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!cacheMapping.keySet().containsAll(ids)) {
 | 
				
			||||||
 | 
					                throw new RuntimeException("Not all requested quizzes cached");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return ids
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .map(id -> {
 | 
				
			||||||
 | 
					                        final QuizData q = cacheMapping.get(id);
 | 
				
			||||||
 | 
					                        return (q == null)
 | 
				
			||||||
 | 
					                                ? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
 | 
				
			||||||
 | 
					                                : Result.of(q);
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .collect(Collectors.toList());
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
 | 
				
			||||||
 | 
					        return this.allQuizzesSupplier.get()
 | 
				
			||||||
 | 
					                .map(LmsAPIService.quizzesFilterFunction(filterMap));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected abstract Supplier<List<QuizData>> allQuizzesSupplier();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -37,6 +37,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx.OpenEdxLmsAPITemplateFactory;
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx.OpenEdxLmsAPITemplateFactory;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleLmsAPITemplateFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Lazy
 | 
					@Lazy
 | 
				
			||||||
@Service
 | 
					@Service
 | 
				
			||||||
| 
						 | 
					@ -49,16 +50,19 @@ public class LmsAPIServiceImpl implements LmsAPIService {
 | 
				
			||||||
    private final ClientCredentialService clientCredentialService;
 | 
					    private final ClientCredentialService clientCredentialService;
 | 
				
			||||||
    private final WebserviceInfo webserviceInfo;
 | 
					    private final WebserviceInfo webserviceInfo;
 | 
				
			||||||
    private final OpenEdxLmsAPITemplateFactory openEdxLmsAPITemplateFactory;
 | 
					    private final OpenEdxLmsAPITemplateFactory openEdxLmsAPITemplateFactory;
 | 
				
			||||||
 | 
					    private final MoodleLmsAPITemplateFactory moodleLmsAPITemplateFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
 | 
					    private final Map<CacheKey, LmsAPITemplate> cache = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public LmsAPIServiceImpl(
 | 
					    public LmsAPIServiceImpl(
 | 
				
			||||||
            final OpenEdxLmsAPITemplateFactory openEdxLmsAPITemplateFactory,
 | 
					            final OpenEdxLmsAPITemplateFactory openEdxLmsAPITemplateFactory,
 | 
				
			||||||
 | 
					            final MoodleLmsAPITemplateFactory moodleLmsAPITemplateFactory,
 | 
				
			||||||
            final LmsSetupDAO lmsSetupDAO,
 | 
					            final LmsSetupDAO lmsSetupDAO,
 | 
				
			||||||
            final ClientCredentialService clientCredentialService,
 | 
					            final ClientCredentialService clientCredentialService,
 | 
				
			||||||
            final WebserviceInfo webserviceInfo) {
 | 
					            final WebserviceInfo webserviceInfo) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.openEdxLmsAPITemplateFactory = openEdxLmsAPITemplateFactory;
 | 
					        this.openEdxLmsAPITemplateFactory = openEdxLmsAPITemplateFactory;
 | 
				
			||||||
 | 
					        this.moodleLmsAPITemplateFactory = moodleLmsAPITemplateFactory;
 | 
				
			||||||
        this.lmsSetupDAO = lmsSetupDAO;
 | 
					        this.lmsSetupDAO = lmsSetupDAO;
 | 
				
			||||||
        this.clientCredentialService = clientCredentialService;
 | 
					        this.clientCredentialService = clientCredentialService;
 | 
				
			||||||
        this.webserviceInfo = webserviceInfo;
 | 
					        this.webserviceInfo = webserviceInfo;
 | 
				
			||||||
| 
						 | 
					@ -227,6 +231,10 @@ public class LmsAPIServiceImpl implements LmsAPIService {
 | 
				
			||||||
                return this.openEdxLmsAPITemplateFactory
 | 
					                return this.openEdxLmsAPITemplateFactory
 | 
				
			||||||
                        .create(lmsSetup, credentials, proxyData)
 | 
					                        .create(lmsSetup, credentials, proxyData)
 | 
				
			||||||
                        .getOrThrow();
 | 
					                        .getOrThrow();
 | 
				
			||||||
 | 
					            case MOODLE:
 | 
				
			||||||
 | 
					                return this.moodleLmsAPITemplateFactory
 | 
				
			||||||
 | 
					                        .create(lmsSetup, credentials, proxyData)
 | 
				
			||||||
 | 
					                        .getOrThrow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            default:
 | 
					            default:
 | 
				
			||||||
                throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType);
 | 
					                throw new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,15 +10,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.net.URL;
 | 
					import java.net.URL;
 | 
				
			||||||
import java.util.ArrayList;
 | 
					import java.util.ArrayList;
 | 
				
			||||||
import java.util.Collection;
 | 
					 | 
				
			||||||
import java.util.HashMap;
 | 
					import java.util.HashMap;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.NoSuchElementException;
 | 
					 | 
				
			||||||
import java.util.Set;
 | 
					 | 
				
			||||||
import java.util.function.Function;
 | 
					 | 
				
			||||||
import java.util.function.Supplier;
 | 
					import java.util.function.Supplier;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.apache.commons.lang3.StringUtils;
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
import org.slf4j.Logger;
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
| 
						 | 
					@ -35,21 +30,21 @@ import org.springframework.security.oauth2.client.http.AccessTokenRequiredExcept
 | 
				
			||||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
 | 
					import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
 | 
				
			||||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
 | 
					import org.springframework.security.oauth2.common.OAuth2AccessToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.Constants;
 | 
					import ch.ethz.seb.sebserver.gbl.Constants;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
 | 
					import ch.ethz.seb.sebserver.gbl.async.AsyncService;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.async.MemoizingCircuitBreaker;
 | 
					 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
 | 
				
			||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
 | 
					import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
 | 
				
			||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Implements the LmsAPITemplate for Open edX LMS Course API access.
 | 
					/** Implements the LmsAPITemplate for Open edX LMS Course API access.
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * See also: https://course-catalog-api-guide.readthedocs.io */
 | 
					 * See also: https://course-catalog-api-guide.readthedocs.io */
 | 
				
			||||||
final class OpenEdxCourseAccess {
 | 
					final class OpenEdxCourseAccess extends CourseAccess {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class);
 | 
					    private static final Logger log = LoggerFactory.getLogger(OpenEdxCourseAccess.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,7 +54,6 @@ final class OpenEdxCourseAccess {
 | 
				
			||||||
    private final LmsSetup lmsSetup;
 | 
					    private final LmsSetup lmsSetup;
 | 
				
			||||||
    private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
 | 
					    private final OpenEdxRestTemplateFactory openEdxRestTemplateFactory;
 | 
				
			||||||
    private final WebserviceInfo webserviceInfo;
 | 
					    private final WebserviceInfo webserviceInfo;
 | 
				
			||||||
    private final MemoizingCircuitBreaker<List<QuizData>> allQuizzesSupplier;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private OAuth2RestTemplate restTemplate;
 | 
					    private OAuth2RestTemplate restTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,16 +63,10 @@ final class OpenEdxCourseAccess {
 | 
				
			||||||
            final WebserviceInfo webserviceInfo,
 | 
					            final WebserviceInfo webserviceInfo,
 | 
				
			||||||
            final AsyncService asyncService) {
 | 
					            final AsyncService asyncService) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(asyncService);
 | 
				
			||||||
        this.lmsSetup = lmsSetup;
 | 
					        this.lmsSetup = lmsSetup;
 | 
				
			||||||
        this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
 | 
					        this.openEdxRestTemplateFactory = openEdxRestTemplateFactory;
 | 
				
			||||||
        this.webserviceInfo = webserviceInfo;
 | 
					        this.webserviceInfo = webserviceInfo;
 | 
				
			||||||
        this.allQuizzesSupplier = asyncService.createMemoizingCircuitBreaker(
 | 
					 | 
				
			||||||
                allQuizzesSupplier(),
 | 
					 | 
				
			||||||
                3,
 | 
					 | 
				
			||||||
                Constants.MINUTE_IN_MILLIS,
 | 
					 | 
				
			||||||
                Constants.MINUTE_IN_MILLIS,
 | 
					 | 
				
			||||||
                true,
 | 
					 | 
				
			||||||
                Constants.HOUR_IN_MILLIS);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    LmsSetupTestResult initAPIAccess() {
 | 
					    LmsSetupTestResult initAPIAccess() {
 | 
				
			||||||
| 
						 | 
					@ -114,51 +102,52 @@ final class OpenEdxCourseAccess {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return LmsSetupTestResult.ofOkay();
 | 
					        return LmsSetupTestResult.ofOkay();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//    Result<QuizData> getQuizFromCache(final String id) {
 | 
				
			||||||
 | 
					//        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					//            return this.allQuizzesSupplier
 | 
				
			||||||
 | 
					//                    .getChached()
 | 
				
			||||||
 | 
					//                    .stream()
 | 
				
			||||||
 | 
					//                    .filter(qd -> id.equals(qd.id))
 | 
				
			||||||
 | 
					//                    .findFirst()
 | 
				
			||||||
 | 
					//                    .orElseThrow(() -> new NoSuchElementException("No cached quiz: " + id));
 | 
				
			||||||
 | 
					//        });
 | 
				
			||||||
 | 
					//    }
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//    Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
 | 
				
			||||||
 | 
					//        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					//            final List<QuizData> cached = this.allQuizzesSupplier.getChached();
 | 
				
			||||||
 | 
					//            if (cached == null) {
 | 
				
			||||||
 | 
					//                throw new RuntimeException("No cached quizzes");
 | 
				
			||||||
 | 
					//            }
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//            final Map<String, QuizData> cacheMapping = cached
 | 
				
			||||||
 | 
					//                    .stream()
 | 
				
			||||||
 | 
					//                    .collect(Collectors.toMap(q -> q.id, Function.identity()));
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//            if (!cacheMapping.keySet().containsAll(ids)) {
 | 
				
			||||||
 | 
					//                throw new RuntimeException("Not all requested quizzes cached");
 | 
				
			||||||
 | 
					//            }
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//            return ids
 | 
				
			||||||
 | 
					//                    .stream()
 | 
				
			||||||
 | 
					//                    .map(id -> {
 | 
				
			||||||
 | 
					//                        final QuizData q = cacheMapping.get(id);
 | 
				
			||||||
 | 
					//                        return (q == null)
 | 
				
			||||||
 | 
					//                                ? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
 | 
				
			||||||
 | 
					//                                : Result.of(q);
 | 
				
			||||||
 | 
					//                    })
 | 
				
			||||||
 | 
					//                    .collect(Collectors.toList());
 | 
				
			||||||
 | 
					//        });
 | 
				
			||||||
 | 
					//    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Result<QuizData> getQuizFromCache(final String id) {
 | 
					//    Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
 | 
				
			||||||
        return Result.tryCatch(() -> {
 | 
					//        return this.allQuizzesSupplier.get()
 | 
				
			||||||
            return this.allQuizzesSupplier
 | 
					//                .map(LmsAPIService.quizzesFilterFunction(filterMap));
 | 
				
			||||||
                    .getChached()
 | 
					//    }
 | 
				
			||||||
                    .stream()
 | 
					 | 
				
			||||||
                    .filter(qd -> id.equals(qd.id))
 | 
					 | 
				
			||||||
                    .findFirst()
 | 
					 | 
				
			||||||
                    .orElseThrow(() -> new NoSuchElementException("No cached quiz: " + id));
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Result<Collection<Result<QuizData>>> getQuizzesFromCache(final Set<String> ids) {
 | 
					    @Override
 | 
				
			||||||
        return Result.tryCatch(() -> {
 | 
					    protected Supplier<List<QuizData>> allQuizzesSupplier() {
 | 
				
			||||||
            final List<QuizData> cached = this.allQuizzesSupplier.getChached();
 | 
					 | 
				
			||||||
            if (cached == null) {
 | 
					 | 
				
			||||||
                throw new RuntimeException("No cached quizzes");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            final Map<String, QuizData> cacheMapping = cached
 | 
					 | 
				
			||||||
                    .stream()
 | 
					 | 
				
			||||||
                    .collect(Collectors.toMap(q -> q.id, Function.identity()));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (!cacheMapping.keySet().containsAll(ids)) {
 | 
					 | 
				
			||||||
                throw new RuntimeException("Not all requested quizzes cached");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return ids
 | 
					 | 
				
			||||||
                    .stream()
 | 
					 | 
				
			||||||
                    .map(id -> {
 | 
					 | 
				
			||||||
                        final QuizData q = cacheMapping.get(id);
 | 
					 | 
				
			||||||
                        return (q == null)
 | 
					 | 
				
			||||||
                                ? Result.<QuizData> ofError(new NoSuchElementException("Quiz with id: " + id))
 | 
					 | 
				
			||||||
                                : Result.of(q);
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
                    .collect(Collectors.toList());
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
 | 
					 | 
				
			||||||
        return this.allQuizzesSupplier.get()
 | 
					 | 
				
			||||||
                .map(LmsAPIService.quizzesFilterFunction(filterMap));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private Supplier<List<QuizData>> allQuizzesSupplier() {
 | 
					 | 
				
			||||||
        return () -> {
 | 
					        return () -> {
 | 
				
			||||||
            return getRestTemplate()
 | 
					            return getRestTemplate()
 | 
				
			||||||
                    .map(this::collectAllQuizzes)
 | 
					                    .map(this::collectAllQuizzes)
 | 
				
			||||||
| 
						 | 
					@ -254,6 +243,7 @@ final class OpenEdxCourseAccess {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Maps a OpenEdX course API course page */
 | 
					    /** Maps a OpenEdX course API course page */
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
    static final class EdXPage {
 | 
					    static final class EdXPage {
 | 
				
			||||||
        public Integer count;
 | 
					        public Integer count;
 | 
				
			||||||
        public String previous;
 | 
					        public String previous;
 | 
				
			||||||
| 
						 | 
					@ -263,6 +253,7 @@ final class OpenEdxCourseAccess {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** Maps the OpenEdX course API course data */
 | 
					    /** Maps the OpenEdX course API course data */
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
    static final class CourseData {
 | 
					    static final class CourseData {
 | 
				
			||||||
        public String id;
 | 
					        public String id;
 | 
				
			||||||
        public String name;
 | 
					        public String name;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -114,6 +114,7 @@ public class OpenEdxCourseRestriction {
 | 
				
			||||||
            final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId);
 | 
					            final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId);
 | 
				
			||||||
            final HttpHeaders httpHeaders = new HttpHeaders();
 | 
					            final HttpHeaders httpHeaders = new HttpHeaders();
 | 
				
			||||||
            httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
 | 
					            httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
 | 
				
			||||||
 | 
					            httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                final OpenEdxSebRestriction data = this.restTemplate.exchange(
 | 
					                final OpenEdxSebRestriction data = this.restTemplate.exchange(
 | 
				
			||||||
| 
						 | 
					@ -199,6 +200,7 @@ public class OpenEdxCourseRestriction {
 | 
				
			||||||
        final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId);
 | 
					        final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId);
 | 
				
			||||||
        final HttpHeaders httpHeaders = new HttpHeaders();
 | 
					        final HttpHeaders httpHeaders = new HttpHeaders();
 | 
				
			||||||
        httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
 | 
					        httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
 | 
				
			||||||
 | 
					        httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
 | 
				
			||||||
        return () -> {
 | 
					        return () -> {
 | 
				
			||||||
            final OpenEdxSebRestriction body = this.restTemplate.exchange(
 | 
					            final OpenEdxSebRestriction body = this.restTemplate.exchange(
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
| 
						 | 
					@ -219,10 +221,12 @@ public class OpenEdxCourseRestriction {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId);
 | 
					        final String url = this.lmsSetup.lmsApiUrl + getSebRestrictionUrl(courseId);
 | 
				
			||||||
        return () -> {
 | 
					        return () -> {
 | 
				
			||||||
 | 
					            final HttpHeaders httpHeaders = new HttpHeaders();
 | 
				
			||||||
 | 
					            httpHeaders.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
 | 
				
			||||||
            final ResponseEntity<Object> exchange = this.restTemplate.exchange(
 | 
					            final ResponseEntity<Object> exchange = this.restTemplate.exchange(
 | 
				
			||||||
                    url,
 | 
					                    url,
 | 
				
			||||||
                    HttpMethod.DELETE,
 | 
					                    HttpMethod.DELETE,
 | 
				
			||||||
                    new HttpEntity<>(new HttpHeaders()),
 | 
					                    new HttpEntity<>(httpHeaders),
 | 
				
			||||||
                    Object.class);
 | 
					                    Object.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) {
 | 
					            if (exchange.getStatusCode() == HttpStatus.NO_CONTENT) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,11 +8,11 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
 | 
					package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.edx;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
import java.util.Collections;
 | 
					import java.util.HashSet;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.slf4j.Logger;
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
import org.slf4j.LoggerFactory;
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
| 
						 | 
					@ -65,20 +65,11 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
 | 
				
			||||||
        return this.openEdxCourseAccess.getQuizzes(filterMap);
 | 
					        return this.openEdxCourseAccess.getQuizzes(filterMap);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
 | 
					 | 
				
			||||||
        return getQuizzes(new FilterMap())
 | 
					 | 
				
			||||||
                .getOrElse(() -> Collections.emptyList())
 | 
					 | 
				
			||||||
                .stream()
 | 
					 | 
				
			||||||
                .filter(quiz -> ids.contains(quiz.id))
 | 
					 | 
				
			||||||
                .map(quiz -> Result.of(quiz))
 | 
					 | 
				
			||||||
                .collect(Collectors.toList());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
    public Result<QuizData> getQuizFromCache(final String id) {
 | 
					    public Result<QuizData> getQuizFromCache(final String id) {
 | 
				
			||||||
        return this.openEdxCourseAccess.getQuizFromCache(id)
 | 
					        return getQuizzesFromCache(new HashSet<>(Arrays.asList(id)))
 | 
				
			||||||
                .orElse(() -> getQuiz(id));
 | 
					                .iterator()
 | 
				
			||||||
 | 
					                .next();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Override
 | 
					    @Override
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,294 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.function.Function;
 | 
				
			||||||
 | 
					import java.util.function.Supplier;
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
 | 
					import org.springframework.util.LinkedMultiValueMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonCreator;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.core.type.TypeReference;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.async.AsyncService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.CourseAccess;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleRestTemplateFactory.MoodleAPIRestTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/** Implements the LmsAPITemplate for Open edX LMS Course API access.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * See also: https://docs.moodle.org/dev/Web_service_API_functions */
 | 
				
			||||||
 | 
					public class MoodleCourseAccess extends CourseAccess {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final Logger log = LoggerFactory.getLogger(MoodleCourseAccess.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String MOOLDE_QUIZ_START_URL_PATH = "/mod/quiz/view.php?id=";
 | 
				
			||||||
 | 
					    private static final String MOODLE_COURSE_API_FUNCTION_NAME = "core_course_get_courses";
 | 
				
			||||||
 | 
					    private static final String MOODLE_QUIZ_API_FUNCTION_NAME = "mod_quiz_get_quizzes_by_courses";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final JSONMapper jsonMapper;
 | 
				
			||||||
 | 
					    private final LmsSetup lmsSetup;
 | 
				
			||||||
 | 
					    private final MoodleRestTemplateFactory moodleRestTemplateFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private MoodleAPIRestTemplate restTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected MoodleCourseAccess(
 | 
				
			||||||
 | 
					            final JSONMapper jsonMapper,
 | 
				
			||||||
 | 
					            final LmsSetup lmsSetup,
 | 
				
			||||||
 | 
					            final MoodleRestTemplateFactory moodleRestTemplateFactory,
 | 
				
			||||||
 | 
					            final AsyncService asyncService) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(asyncService);
 | 
				
			||||||
 | 
					        this.jsonMapper = jsonMapper;
 | 
				
			||||||
 | 
					        this.lmsSetup = lmsSetup;
 | 
				
			||||||
 | 
					        this.moodleRestTemplateFactory = moodleRestTemplateFactory;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    LmsSetupTestResult initAPIAccess() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final LmsSetupTestResult attributesCheck = this.moodleRestTemplateFactory.test();
 | 
				
			||||||
 | 
					        if (!attributesCheck.isOk()) {
 | 
				
			||||||
 | 
					            return attributesCheck;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final Result<MoodleAPIRestTemplate> restTemplateRequest = getRestTemplate();
 | 
				
			||||||
 | 
					        if (restTemplateRequest.hasError()) {
 | 
				
			||||||
 | 
					            final String message = "Failed to gain access token from Moodle Rest API:\n tried token endpoints: " +
 | 
				
			||||||
 | 
					                    this.moodleRestTemplateFactory.knownTokenAccessPaths;
 | 
				
			||||||
 | 
					            log.error(message, restTemplateRequest.getError().getMessage());
 | 
				
			||||||
 | 
					            return LmsSetupTestResult.ofTokenRequestError(message);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final MoodleAPIRestTemplate restTemplate = restTemplateRequest.get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            restTemplate.testAPIConnection(
 | 
				
			||||||
 | 
					                    MOODLE_COURSE_API_FUNCTION_NAME,
 | 
				
			||||||
 | 
					                    MOODLE_QUIZ_API_FUNCTION_NAME);
 | 
				
			||||||
 | 
					        } catch (final RuntimeException e) {
 | 
				
			||||||
 | 
					            log.error("Failed to access Open edX course API: ", e);
 | 
				
			||||||
 | 
					            return LmsSetupTestResult.ofQuizAccessAPIError(e.getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return LmsSetupTestResult.ofOkay();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected Supplier<List<QuizData>> allQuizzesSupplier() {
 | 
				
			||||||
 | 
					        return () -> {
 | 
				
			||||||
 | 
					            return getRestTemplate()
 | 
				
			||||||
 | 
					                    .map(this::collectAllQuizzes)
 | 
				
			||||||
 | 
					                    .getOrThrow();
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private ArrayList<QuizData> collectAllQuizzes(final MoodleAPIRestTemplate restTemplate) {
 | 
				
			||||||
 | 
					        final String urlPrefix = this.lmsSetup.lmsApiUrl + MOOLDE_QUIZ_START_URL_PATH;
 | 
				
			||||||
 | 
					        return collectAllCourses(
 | 
				
			||||||
 | 
					                restTemplate)
 | 
				
			||||||
 | 
					                        .stream()
 | 
				
			||||||
 | 
					                        .reduce(
 | 
				
			||||||
 | 
					                                new ArrayList<QuizData>(),
 | 
				
			||||||
 | 
					                                (list, courseData) -> {
 | 
				
			||||||
 | 
					                                    list.addAll(quizDataOf(
 | 
				
			||||||
 | 
					                                            this.lmsSetup,
 | 
				
			||||||
 | 
					                                            courseData,
 | 
				
			||||||
 | 
					                                            urlPrefix));
 | 
				
			||||||
 | 
					                                    return list;
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                (list1, list2) -> {
 | 
				
			||||||
 | 
					                                    list1.addAll(list2);
 | 
				
			||||||
 | 
					                                    return list1;
 | 
				
			||||||
 | 
					                                });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private List<CourseData> collectAllCourses(final MoodleAPIRestTemplate restTemplate) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // first get courses form Moodle...
 | 
				
			||||||
 | 
					            final String coursesJSON = restTemplate.callMoodleAPIFunction(MOODLE_COURSE_API_FUNCTION_NAME);
 | 
				
			||||||
 | 
					            final Map<String, CourseData> courseData = this.jsonMapper.<Collection<CourseData>> readValue(
 | 
				
			||||||
 | 
					                    coursesJSON,
 | 
				
			||||||
 | 
					                    new TypeReference<Collection<CourseData>>() {
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .collect(Collectors.toMap(d -> d.id, Function.identity()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // then get all quizzes of courses and filter
 | 
				
			||||||
 | 
					            final LinkedMultiValueMap<String, String> attributes = new LinkedMultiValueMap<>();
 | 
				
			||||||
 | 
					            attributes.put("courseids", new ArrayList<>(courseData.keySet()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final String quizzesJSON = restTemplate.callMoodleAPIFunction(
 | 
				
			||||||
 | 
					                    MOODLE_QUIZ_API_FUNCTION_NAME,
 | 
				
			||||||
 | 
					                    attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final CourseQuizData courseQuizData = this.jsonMapper.readValue(
 | 
				
			||||||
 | 
					                    quizzesJSON,
 | 
				
			||||||
 | 
					                    CourseQuizData.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            courseQuizData.quizzes
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .forEach(quiz -> {
 | 
				
			||||||
 | 
					                        final CourseData course = courseData.get(quiz.course);
 | 
				
			||||||
 | 
					                        if (course != null) {
 | 
				
			||||||
 | 
					                            course.quizzes.add(quiz);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return courseData.values()
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .filter(c -> !c.quizzes.isEmpty())
 | 
				
			||||||
 | 
					                    .collect(Collectors.toList());
 | 
				
			||||||
 | 
					        } catch (final Exception e) {
 | 
				
			||||||
 | 
					            throw new RuntimeException("Unexpected exception while trying to get course data: ", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static List<QuizData> quizDataOf(
 | 
				
			||||||
 | 
					            final LmsSetup lmsSetup,
 | 
				
			||||||
 | 
					            final CourseData courseData,
 | 
				
			||||||
 | 
					            final String uriPrefix) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final Map<String, String> additionalAttrs = new HashMap<>();
 | 
				
			||||||
 | 
					        additionalAttrs.clear();
 | 
				
			||||||
 | 
					        additionalAttrs.put("timecreated", String.valueOf(courseData.timecreated));
 | 
				
			||||||
 | 
					        additionalAttrs.put("course_shortname", courseData.shortname);
 | 
				
			||||||
 | 
					        additionalAttrs.put("course_fullname", courseData.fullname);
 | 
				
			||||||
 | 
					        additionalAttrs.put("course_displayname", courseData.displayname);
 | 
				
			||||||
 | 
					        additionalAttrs.put("course_summary", courseData.summary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return courseData.quizzes
 | 
				
			||||||
 | 
					                .stream()
 | 
				
			||||||
 | 
					                .map(courseQuizData -> {
 | 
				
			||||||
 | 
					                    final String startURI = uriPrefix + courseData.id;
 | 
				
			||||||
 | 
					                    additionalAttrs.put("coursemodule", courseQuizData.coursemodule);
 | 
				
			||||||
 | 
					                    additionalAttrs.put("timelimit", String.valueOf(courseQuizData.timelimit));
 | 
				
			||||||
 | 
					                    return new QuizData(
 | 
				
			||||||
 | 
					                            courseQuizData.id,
 | 
				
			||||||
 | 
					                            lmsSetup.getInstitutionId(),
 | 
				
			||||||
 | 
					                            lmsSetup.id,
 | 
				
			||||||
 | 
					                            lmsSetup.getLmsType(),
 | 
				
			||||||
 | 
					                            courseQuizData.name,
 | 
				
			||||||
 | 
					                            courseQuizData.intro,
 | 
				
			||||||
 | 
					                            Utils.toDateTimeUTCUnix(courseData.startdate),
 | 
				
			||||||
 | 
					                            Utils.toDateTimeUTCUnix(courseData.enddate),
 | 
				
			||||||
 | 
					                            startURI,
 | 
				
			||||||
 | 
					                            additionalAttrs);
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .collect(Collectors.toList());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private Result<MoodleAPIRestTemplate> getRestTemplate() {
 | 
				
			||||||
 | 
					        if (this.restTemplate == null) {
 | 
				
			||||||
 | 
					            final Result<MoodleAPIRestTemplate> templateRequest = this.moodleRestTemplateFactory
 | 
				
			||||||
 | 
					                    .createRestTemplate();
 | 
				
			||||||
 | 
					            if (templateRequest.hasError()) {
 | 
				
			||||||
 | 
					                return templateRequest;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.restTemplate = templateRequest.get();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Result.of(this.restTemplate);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** Maps the Moodle course API course data */
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
 | 
					    static final class CourseData {
 | 
				
			||||||
 | 
					        final String id;
 | 
				
			||||||
 | 
					        final String shortname;
 | 
				
			||||||
 | 
					        final String fullname;
 | 
				
			||||||
 | 
					        final String displayname;
 | 
				
			||||||
 | 
					        final String summary;
 | 
				
			||||||
 | 
					        final Long startdate; // unixtime milliseconds UTC
 | 
				
			||||||
 | 
					        final Long enddate; // unixtime milliseconds UTC
 | 
				
			||||||
 | 
					        final Long timecreated; // unixtime milliseconds UTC
 | 
				
			||||||
 | 
					        final Collection<CourseQuiz> quizzes = new ArrayList<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @JsonCreator
 | 
				
			||||||
 | 
					        protected CourseData(
 | 
				
			||||||
 | 
					                @JsonProperty(value = "id") final String id,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "shortname") final String shortname,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "fullname") final String fullname,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "displayname") final String displayname,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "summary") final String summary,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "startdate") final Long startdate,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "enddate") final Long enddate,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "timecreated") final Long timecreated) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.id = id;
 | 
				
			||||||
 | 
					            this.shortname = shortname;
 | 
				
			||||||
 | 
					            this.fullname = fullname;
 | 
				
			||||||
 | 
					            this.displayname = displayname;
 | 
				
			||||||
 | 
					            this.summary = summary;
 | 
				
			||||||
 | 
					            this.startdate = startdate;
 | 
				
			||||||
 | 
					            this.enddate = enddate;
 | 
				
			||||||
 | 
					            this.timecreated = timecreated;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
 | 
					    static final class CourseQuizData {
 | 
				
			||||||
 | 
					        final Collection<CourseQuiz> quizzes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @JsonCreator
 | 
				
			||||||
 | 
					        protected CourseQuizData(
 | 
				
			||||||
 | 
					                @JsonProperty(value = "quizzes") final Collection<CourseQuiz> quizzes) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.quizzes = quizzes;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
 | 
					    static final class CourseQuiz {
 | 
				
			||||||
 | 
					        final String id;
 | 
				
			||||||
 | 
					        final String course;
 | 
				
			||||||
 | 
					        final String coursemodule;
 | 
				
			||||||
 | 
					        final String name;
 | 
				
			||||||
 | 
					        final String intro; // HTML
 | 
				
			||||||
 | 
					        final Long timelimit; // unixtime milliseconds UTC
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @JsonCreator
 | 
				
			||||||
 | 
					        protected CourseQuiz(
 | 
				
			||||||
 | 
					                @JsonProperty(value = "id") final String id,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "course") final String course,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "coursemodule") final String coursemodule,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "name") final String name,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "intro") final String intro,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "timelimit") final Long timelimit) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.id = id;
 | 
				
			||||||
 | 
					            this.course = course;
 | 
				
			||||||
 | 
					            this.coursemodule = coursemodule;
 | 
				
			||||||
 | 
					            this.name = name;
 | 
				
			||||||
 | 
					            this.intro = intro;
 | 
				
			||||||
 | 
					            this.timelimit = timelimit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,87 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.exam.SebRestriction;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class MoodleLmsAPITemplate implements LmsAPITemplate {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final LmsSetup lmsSetup;
 | 
				
			||||||
 | 
					    private final MoodleCourseAccess moodleCourseAccess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected MoodleLmsAPITemplate(
 | 
				
			||||||
 | 
					            final LmsSetup lmsSetup,
 | 
				
			||||||
 | 
					            final MoodleCourseAccess moodleCourseAccess) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.lmsSetup = lmsSetup;
 | 
				
			||||||
 | 
					        this.moodleCourseAccess = moodleCourseAccess;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public LmsSetup lmsSetup() {
 | 
				
			||||||
 | 
					        return this.lmsSetup;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public LmsSetupTestResult testCourseAccessAPI() {
 | 
				
			||||||
 | 
					        return this.moodleCourseAccess.initAPIAccess();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public LmsSetupTestResult testCourseRestrictionAPI() {
 | 
				
			||||||
 | 
					        return LmsSetupTestResult.ofQuizRestrictionAPIError("Not available yet");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public Result<List<QuizData>> getQuizzes(final FilterMap filterMap) {
 | 
				
			||||||
 | 
					        return this.moodleCourseAccess.getQuizzes(filterMap);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public Collection<Result<QuizData>> getQuizzesFromCache(final Set<String> ids) {
 | 
				
			||||||
 | 
					        return this.moodleCourseAccess.getQuizzesFromCache(ids)
 | 
				
			||||||
 | 
					                .getOrElse(() -> getQuizzes(ids));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public Result<QuizData> getQuizFromCache(final String id) {
 | 
				
			||||||
 | 
					        return this.moodleCourseAccess.getQuizFromCache(id)
 | 
				
			||||||
 | 
					                .orElse(() -> getQuiz(id));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public Result<SebRestriction> getSebClientRestriction(final Exam exam) {
 | 
				
			||||||
 | 
					        throw new UnsupportedOperationException("SEB Restriction API not available yet");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public Result<SebRestriction> applySebClientRestriction(
 | 
				
			||||||
 | 
					            final String externalExamId,
 | 
				
			||||||
 | 
					            final SebRestriction sebRestrictionData) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        throw new UnsupportedOperationException("SEB Restriction API not available yet");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public Result<Exam> releaseSebClientRestriction(final Exam exam) {
 | 
				
			||||||
 | 
					        throw new UnsupportedOperationException("SEB Restriction API not available yet");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,82 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
 | 
					import org.springframework.beans.factory.annotation.Value;
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Lazy;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.Constants;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.async.AsyncService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.client.ProxyData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Lazy
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					@WebServiceProfile
 | 
				
			||||||
 | 
					public class MoodleLmsAPITemplateFactory {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final JSONMapper jsonMapper;
 | 
				
			||||||
 | 
					    private final AsyncService asyncService;
 | 
				
			||||||
 | 
					    private final ClientCredentialService clientCredentialService;
 | 
				
			||||||
 | 
					    private final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
 | 
				
			||||||
 | 
					    private final String[] alternativeTokenRequestPaths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected MoodleLmsAPITemplateFactory(
 | 
				
			||||||
 | 
					            final JSONMapper jsonMapper,
 | 
				
			||||||
 | 
					            final AsyncService asyncService,
 | 
				
			||||||
 | 
					            final ClientCredentialService clientCredentialService,
 | 
				
			||||||
 | 
					            final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
 | 
				
			||||||
 | 
					            @Value("${sebserver.webservice.lms.moodle.api.token.request.paths}") final String alternativeTokenRequestPaths) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.jsonMapper = jsonMapper;
 | 
				
			||||||
 | 
					        this.asyncService = asyncService;
 | 
				
			||||||
 | 
					        this.clientCredentialService = clientCredentialService;
 | 
				
			||||||
 | 
					        this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
 | 
				
			||||||
 | 
					        this.alternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
 | 
				
			||||||
 | 
					                ? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR)
 | 
				
			||||||
 | 
					                : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Result<MoodleLmsAPITemplate> create(
 | 
				
			||||||
 | 
					            final LmsSetup lmsSetup,
 | 
				
			||||||
 | 
					            final ClientCredentials credentials,
 | 
				
			||||||
 | 
					            final ProxyData proxyData) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final MoodleRestTemplateFactory moodleRestTemplateFactory = new MoodleRestTemplateFactory(
 | 
				
			||||||
 | 
					                    this.jsonMapper,
 | 
				
			||||||
 | 
					                    lmsSetup,
 | 
				
			||||||
 | 
					                    credentials,
 | 
				
			||||||
 | 
					                    proxyData,
 | 
				
			||||||
 | 
					                    this.clientCredentialService,
 | 
				
			||||||
 | 
					                    this.clientHttpRequestFactoryService,
 | 
				
			||||||
 | 
					                    this.alternativeTokenRequestPaths);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final MoodleCourseAccess moodleCourseAccess = new MoodleCourseAccess(
 | 
				
			||||||
 | 
					                    this.jsonMapper,
 | 
				
			||||||
 | 
					                    lmsSetup,
 | 
				
			||||||
 | 
					                    moodleRestTemplateFactory,
 | 
				
			||||||
 | 
					                    this.asyncService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return new MoodleLmsAPITemplate(
 | 
				
			||||||
 | 
					                    lmsSetup,
 | 
				
			||||||
 | 
					                    moodleCourseAccess);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,391 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This Source Code Form is subject to the terms of the Mozilla Public
 | 
				
			||||||
 | 
					 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
				
			||||||
 | 
					 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.net.URISyntaxException;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.HashSet;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					import java.util.function.Function;
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpEntity;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpHeaders;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpMethod;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
 | 
					import org.springframework.http.MediaType;
 | 
				
			||||||
 | 
					import org.springframework.http.ResponseEntity;
 | 
				
			||||||
 | 
					import org.springframework.http.client.ClientHttpRequestFactory;
 | 
				
			||||||
 | 
					import org.springframework.util.MultiValueMap;
 | 
				
			||||||
 | 
					import org.springframework.web.client.RestTemplate;
 | 
				
			||||||
 | 
					import org.springframework.web.util.UriComponentsBuilder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonCreator;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.APIMessage;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Result;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.gbl.util.Utils;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
 | 
				
			||||||
 | 
					import ch.ethz.seb.sebserver.webservice.servicelayer.client.ProxyData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class MoodleRestTemplateFactory {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final JSONMapper jsonMapper;
 | 
				
			||||||
 | 
					    final LmsSetup lmsSetup;
 | 
				
			||||||
 | 
					    final ClientCredentials credentials;
 | 
				
			||||||
 | 
					    final ProxyData proxyData;
 | 
				
			||||||
 | 
					    final ClientHttpRequestFactoryService clientHttpRequestFactoryService;
 | 
				
			||||||
 | 
					    final ClientCredentialService clientCredentialService;
 | 
				
			||||||
 | 
					    final Set<String> knownTokenAccessPaths;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public MoodleRestTemplateFactory(
 | 
				
			||||||
 | 
					            final JSONMapper jsonMapper,
 | 
				
			||||||
 | 
					            final LmsSetup lmsSetup,
 | 
				
			||||||
 | 
					            final ClientCredentials credentials,
 | 
				
			||||||
 | 
					            final ProxyData proxyData,
 | 
				
			||||||
 | 
					            final ClientCredentialService clientCredentialService,
 | 
				
			||||||
 | 
					            final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
 | 
				
			||||||
 | 
					            final String[] alternativeTokenRequestPaths) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.jsonMapper = jsonMapper;
 | 
				
			||||||
 | 
					        this.lmsSetup = lmsSetup;
 | 
				
			||||||
 | 
					        this.clientCredentialService = clientCredentialService;
 | 
				
			||||||
 | 
					        this.credentials = credentials;
 | 
				
			||||||
 | 
					        this.proxyData = proxyData;
 | 
				
			||||||
 | 
					        this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.knownTokenAccessPaths = new HashSet<>();
 | 
				
			||||||
 | 
					        this.knownTokenAccessPaths.add(MoodleAPIRestTemplate.MOODLE_DEFAULT_TOKEN_REQUEST_PATH);
 | 
				
			||||||
 | 
					        if (alternativeTokenRequestPaths != null) {
 | 
				
			||||||
 | 
					            this.knownTokenAccessPaths.addAll(Arrays.asList(alternativeTokenRequestPaths));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public LmsSetupTestResult test() {
 | 
				
			||||||
 | 
					        final List<APIMessage> missingAttrs = new ArrayList<>();
 | 
				
			||||||
 | 
					        if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) {
 | 
				
			||||||
 | 
					            missingAttrs.add(APIMessage.fieldValidationError(
 | 
				
			||||||
 | 
					                    LMS_SETUP.ATTR_LMS_URL,
 | 
				
			||||||
 | 
					                    "lmsSetup:lmsUrl:notNull"));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (StringUtils.isBlank(this.lmsSetup.lmsRestApiToken)) {
 | 
				
			||||||
 | 
					            if (!this.credentials.hasClientId()) {
 | 
				
			||||||
 | 
					                missingAttrs.add(APIMessage.fieldValidationError(
 | 
				
			||||||
 | 
					                        LMS_SETUP.ATTR_LMS_CLIENTNAME,
 | 
				
			||||||
 | 
					                        "lmsSetup:lmsClientname:notNull"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!this.credentials.hasSecret()) {
 | 
				
			||||||
 | 
					                missingAttrs.add(APIMessage.fieldValidationError(
 | 
				
			||||||
 | 
					                        LMS_SETUP.ATTR_LMS_CLIENTSECRET,
 | 
				
			||||||
 | 
					                        "lmsSetup:lmsClientsecret:notNull"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!missingAttrs.isEmpty()) {
 | 
				
			||||||
 | 
					            return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return LmsSetupTestResult.ofOkay();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Result<MoodleAPIRestTemplate> createRestTemplate() {
 | 
				
			||||||
 | 
					        return this.knownTokenAccessPaths
 | 
				
			||||||
 | 
					                .stream()
 | 
				
			||||||
 | 
					                .map(this::createRestTemplate)
 | 
				
			||||||
 | 
					                .filter(Result::hasValue)
 | 
				
			||||||
 | 
					                .findFirst()
 | 
				
			||||||
 | 
					                .orElse(Result.ofRuntimeError(
 | 
				
			||||||
 | 
					                        "Failed to gain any access on paths: " + this.knownTokenAccessPaths));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Result<MoodleAPIRestTemplate> createRestTemplate(final String accessTokenPath) {
 | 
				
			||||||
 | 
					        return Result.tryCatch(() -> {
 | 
				
			||||||
 | 
					            final MoodleAPIRestTemplate template = createRestTemplate(
 | 
				
			||||||
 | 
					                    this.credentials,
 | 
				
			||||||
 | 
					                    accessTokenPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final CharSequence accessToken = template.getAccessToken();
 | 
				
			||||||
 | 
					            if (accessToken == null) {
 | 
				
			||||||
 | 
					                throw new RuntimeException("Failed to gain access token on path: " + accessTokenPath);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return template;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private MoodleAPIRestTemplate createRestTemplate(
 | 
				
			||||||
 | 
					            final ClientCredentials credentials,
 | 
				
			||||||
 | 
					            final String accessTokenRequestPath) throws URISyntaxException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final CharSequence plainClientId = credentials.clientId;
 | 
				
			||||||
 | 
					        final CharSequence plainClientSecret = this.clientCredentialService.getPlainClientSecret(credentials);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final MoodleAPIRestTemplate restTemplate = new MoodleAPIRestTemplate(
 | 
				
			||||||
 | 
					                this.jsonMapper,
 | 
				
			||||||
 | 
					                this.lmsSetup.lmsApiUrl,
 | 
				
			||||||
 | 
					                accessTokenRequestPath,
 | 
				
			||||||
 | 
					                this.lmsSetup.lmsRestApiToken,
 | 
				
			||||||
 | 
					                plainClientId,
 | 
				
			||||||
 | 
					                plainClientSecret);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final ClientHttpRequestFactory clientHttpRequestFactory = this.clientHttpRequestFactoryService
 | 
				
			||||||
 | 
					                .getClientHttpRequestFactory(this.proxyData)
 | 
				
			||||||
 | 
					                .getOrThrow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        restTemplate.setRequestFactory(clientHttpRequestFactory);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return restTemplate;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public final class MoodleAPIRestTemplate extends RestTemplate {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static final String URI_VAR_USER_NAME = "username";
 | 
				
			||||||
 | 
					        public static final String URI_VAR_PASSWORD = "pwd";
 | 
				
			||||||
 | 
					        public static final String URI_VAR_SERVICE = "service";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private static final String MOODLE_DEFAULT_TOKEN_REQUEST_PATH =
 | 
				
			||||||
 | 
					                "/login/token.php?username={" + URI_VAR_USER_NAME +
 | 
				
			||||||
 | 
					                        "}&password={" + URI_VAR_PASSWORD + "}&service={" + URI_VAR_SERVICE + "}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private static final String MOODLE_DEFAULT_REST_API_PATH = "/webservice/rest/server.php";
 | 
				
			||||||
 | 
					        private static final String REST_REQUEST_TOKEN_NAME = "wstoken";
 | 
				
			||||||
 | 
					        private static final String REST_REQUEST_FUNCTION_NAME = "wsfunction";
 | 
				
			||||||
 | 
					        private static final String REST_REQUEST_FORMAT_NAME = "moodlewsrestformat";
 | 
				
			||||||
 | 
					        private static final String REST_API_TEST_FUNCTION = "core_webservice_get_site_info";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private final String serverURL;
 | 
				
			||||||
 | 
					        private final String tokenPath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private CharSequence accessToken = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private final Map<String, String> tokenReqURIVars;
 | 
				
			||||||
 | 
					        private final HttpEntity<?> tokenReqEntity = new HttpEntity<>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        protected MoodleAPIRestTemplate(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                final JSONMapper jsonMapper,
 | 
				
			||||||
 | 
					                final String serverURL,
 | 
				
			||||||
 | 
					                final String tokenPath,
 | 
				
			||||||
 | 
					                final CharSequence accessToken,
 | 
				
			||||||
 | 
					                final CharSequence username,
 | 
				
			||||||
 | 
					                final CharSequence password) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.serverURL = serverURL;
 | 
				
			||||||
 | 
					            this.tokenPath = tokenPath;
 | 
				
			||||||
 | 
					            this.accessToken = StringUtils.isNotBlank(accessToken) ? accessToken : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.tokenReqURIVars = new HashMap<>();
 | 
				
			||||||
 | 
					            this.tokenReqURIVars.put(URI_VAR_USER_NAME, String.valueOf(username));
 | 
				
			||||||
 | 
					            this.tokenReqURIVars.put(URI_VAR_PASSWORD, String.valueOf(password));
 | 
				
			||||||
 | 
					            this.tokenReqURIVars.put(URI_VAR_SERVICE, "moodle_mobile_app");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String getService() {
 | 
				
			||||||
 | 
					            return this.tokenReqURIVars.get(URI_VAR_SERVICE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public void setService(final String service) {
 | 
				
			||||||
 | 
					            this.tokenReqURIVars.put(URI_VAR_SERVICE, service);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public CharSequence getAccessToken() {
 | 
				
			||||||
 | 
					            if (this.accessToken == null) {
 | 
				
			||||||
 | 
					                requestAccessToken();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return this.accessToken;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public void testAPIConnection(final String... functions) {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                final String apiInfo = this.callMoodleAPIFunction(REST_API_TEST_FUNCTION);
 | 
				
			||||||
 | 
					                final WebserviceInfo webserviceInfo =
 | 
				
			||||||
 | 
					                        MoodleRestTemplateFactory.this.jsonMapper.readValue(apiInfo, WebserviceInfo.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (StringUtils.isBlank(webserviceInfo.username) || StringUtils.isBlank(webserviceInfo.userid)) {
 | 
				
			||||||
 | 
					                    throw new RuntimeException("Ivalid WebserviceInfo: " + webserviceInfo);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                final List<String> missingAPIFunctions = Arrays.asList(functions)
 | 
				
			||||||
 | 
					                        .stream()
 | 
				
			||||||
 | 
					                        .filter(f -> !webserviceInfo.functions.containsKey(f))
 | 
				
			||||||
 | 
					                        .collect(Collectors.toList());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!missingAPIFunctions.isEmpty()) {
 | 
				
			||||||
 | 
					                    throw new RuntimeException("Missing Moodle Webservice API functions: " + missingAPIFunctions);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            } catch (final RuntimeException re) {
 | 
				
			||||||
 | 
					                throw re;
 | 
				
			||||||
 | 
					            } catch (final Exception e) {
 | 
				
			||||||
 | 
					                throw new RuntimeException("Failed to test Moodle rest API: ", e);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String callMoodleAPIFunction(final String functionName) {
 | 
				
			||||||
 | 
					            return callMoodleAPIFunction(functionName, null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public String callMoodleAPIFunction(
 | 
				
			||||||
 | 
					                final String functionName,
 | 
				
			||||||
 | 
					                final MultiValueMap<String, String> queryAttributes) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            getAccessToken();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final UriComponentsBuilder queryParam = UriComponentsBuilder
 | 
				
			||||||
 | 
					                    .fromHttpUrl(this.serverURL + MOODLE_DEFAULT_REST_API_PATH)
 | 
				
			||||||
 | 
					                    .queryParam(REST_REQUEST_TOKEN_NAME, this.accessToken)
 | 
				
			||||||
 | 
					                    .queryParam(REST_REQUEST_FUNCTION_NAME, functionName)
 | 
				
			||||||
 | 
					                    .queryParam(REST_REQUEST_FORMAT_NAME, "json");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final boolean usePOST = queryAttributes != null && !queryAttributes.isEmpty();
 | 
				
			||||||
 | 
					            HttpEntity<?> functionReqEntity;
 | 
				
			||||||
 | 
					            if (usePOST) {
 | 
				
			||||||
 | 
					                final HttpHeaders headers = new HttpHeaders();
 | 
				
			||||||
 | 
					                headers.set(
 | 
				
			||||||
 | 
					                        HttpHeaders.CONTENT_TYPE,
 | 
				
			||||||
 | 
					                        MediaType.APPLICATION_FORM_URLENCODED_VALUE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                final String body = Utils.toAppFormUrlEncodedBody(queryAttributes);
 | 
				
			||||||
 | 
					                functionReqEntity = new HttpEntity<>(body, headers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                functionReqEntity = new HttpEntity<>(null);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//            // NOTE: The interpretation of a multi-value GET parameter on a URL quesry part
 | 
				
			||||||
 | 
					//            //       seems to be very PHP specific. It must have the form of:
 | 
				
			||||||
 | 
					//            //       ... ¶m[]=x¶m[]=y& ...
 | 
				
			||||||
 | 
					//            //       And the square bracket must not be escaped on the URL like: %5B%5D
 | 
				
			||||||
 | 
					//            String urlString = queryParam.toUriString()
 | 
				
			||||||
 | 
					//            .replaceAll("%5B", "[")
 | 
				
			||||||
 | 
					//            .replaceAll("%5D", "]");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final ResponseEntity<String> response = super.exchange(
 | 
				
			||||||
 | 
					                    queryParam.toUriString(),
 | 
				
			||||||
 | 
					                    usePOST ? HttpMethod.POST : HttpMethod.GET,
 | 
				
			||||||
 | 
					                    functionReqEntity,
 | 
				
			||||||
 | 
					                    String.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (response.getStatusCode() != HttpStatus.OK) {
 | 
				
			||||||
 | 
					                throw new RuntimeException(
 | 
				
			||||||
 | 
					                        "Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
 | 
				
			||||||
 | 
					                                MoodleRestTemplateFactory.this.lmsSetup + " response: " + response.getBody());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final String body = response.getBody();
 | 
				
			||||||
 | 
					            // NOTE: for some unknown reason, Moodles API error responses come with a 200 OK response HTTP Status
 | 
				
			||||||
 | 
					            //       So this is a special Moodle specific error handling here...
 | 
				
			||||||
 | 
					            if (body.startsWith("{exception")) {
 | 
				
			||||||
 | 
					                throw new RuntimeException(
 | 
				
			||||||
 | 
					                        "Failed to call Moodle webservice API function: " + functionName + " lms setup: " +
 | 
				
			||||||
 | 
					                                MoodleRestTemplateFactory.this.lmsSetup + " response: " + body);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return body;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private void requestAccessToken() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            final ResponseEntity<String> response = super.exchange(
 | 
				
			||||||
 | 
					                    this.serverURL + this.tokenPath,
 | 
				
			||||||
 | 
					                    HttpMethod.GET,
 | 
				
			||||||
 | 
					                    this.tokenReqEntity,
 | 
				
			||||||
 | 
					                    String.class,
 | 
				
			||||||
 | 
					                    this.tokenReqURIVars);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (response.getStatusCode() != HttpStatus.OK) {
 | 
				
			||||||
 | 
					                throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
 | 
				
			||||||
 | 
					                        MoodleRestTemplateFactory.this.lmsSetup + " response: " + response.getBody());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                final MoodleToken moodleToken = MoodleRestTemplateFactory.this.jsonMapper.readValue(
 | 
				
			||||||
 | 
					                        response.getBody(),
 | 
				
			||||||
 | 
					                        MoodleToken.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.accessToken = moodleToken.token;
 | 
				
			||||||
 | 
					            } catch (final Exception e) {
 | 
				
			||||||
 | 
					                throw new RuntimeException("Failed to gain access token for LMS (Moodle): lmsSetup: " +
 | 
				
			||||||
 | 
					                        MoodleRestTemplateFactory.this.lmsSetup + " response: " + response.getBody(), e);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
 | 
					    private final static class MoodleToken {
 | 
				
			||||||
 | 
					        final String token;
 | 
				
			||||||
 | 
					        @SuppressWarnings("unused")
 | 
				
			||||||
 | 
					        final String privatetoken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @JsonCreator
 | 
				
			||||||
 | 
					        protected MoodleToken(
 | 
				
			||||||
 | 
					                @JsonProperty(value = "token") final String token,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "privatetoken", required = false) final String privatetoken) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.token = token;
 | 
				
			||||||
 | 
					            this.privatetoken = privatetoken;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
 | 
					    private final static class WebserviceInfo {
 | 
				
			||||||
 | 
					        String username;
 | 
				
			||||||
 | 
					        String userid;
 | 
				
			||||||
 | 
					        Map<String, FunctionInfo> functions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @JsonCreator
 | 
				
			||||||
 | 
					        protected WebserviceInfo(
 | 
				
			||||||
 | 
					                @JsonProperty(value = "username") final String username,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "userid") final String userid,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "functions") final Collection<FunctionInfo> functions) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.username = username;
 | 
				
			||||||
 | 
					            this.userid = userid;
 | 
				
			||||||
 | 
					            this.functions = functions
 | 
				
			||||||
 | 
					                    .stream()
 | 
				
			||||||
 | 
					                    .collect(Collectors.toMap(fi -> fi.name, Function.identity()));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @JsonIgnoreProperties(ignoreUnknown = true)
 | 
				
			||||||
 | 
					    private final static class FunctionInfo {
 | 
				
			||||||
 | 
					        String name;
 | 
				
			||||||
 | 
					        @SuppressWarnings("unused")
 | 
				
			||||||
 | 
					        String version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @JsonCreator
 | 
				
			||||||
 | 
					        protected FunctionInfo(
 | 
				
			||||||
 | 
					                @JsonProperty(value = "name") final String name,
 | 
				
			||||||
 | 
					                @JsonProperty(value = "version") final String version) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.name = name;
 | 
				
			||||||
 | 
					            this.version = version;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -158,7 +158,7 @@ class ExamSessionControlTask implements DisposableBean {
 | 
				
			||||||
            final Map<Long, String> updated = this.examDAO.allForEndCheck()
 | 
					            final Map<Long, String> updated = this.examDAO.allForEndCheck()
 | 
				
			||||||
                    .getOrThrow()
 | 
					                    .getOrThrow()
 | 
				
			||||||
                    .stream()
 | 
					                    .stream()
 | 
				
			||||||
                    .filter(exam -> exam.endTime.plus(this.examTimeSuffix).isBefore(now))
 | 
					                    .filter(exam -> exam.endTime != null && exam.endTime.plus(this.examTimeSuffix).isBefore(now))
 | 
				
			||||||
                    .map(exam -> this.examUpdateHandler.setFinished(exam, updateId))
 | 
					                    .map(exam -> this.examUpdateHandler.setFinished(exam, updateId))
 | 
				
			||||||
                    .collect(Collectors.toMap(Exam::getId, Exam::getName));
 | 
					                    .collect(Collectors.toMap(Exam::getId, Exam::getName));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ spring.datasource.hikari.maxLifetime=1800000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sebserver.http.client.connect-timeout=15000
 | 
					sebserver.http.client.connect-timeout=15000
 | 
				
			||||||
sebserver.http.client.connection-request-timeout=10000
 | 
					sebserver.http.client.connection-request-timeout=10000
 | 
				
			||||||
sebserver.http.client.read-timeout=10000
 | 
					sebserver.http.client.read-timeout=20000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# webservice configuration
 | 
					# webservice configuration
 | 
				
			||||||
sebserver.init.adminaccount.gen-on-init=false
 | 
					sebserver.init.adminaccount.gen-on-init=false
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,7 @@ sebserver.webservice.api.exam.enable-indicator-cache=true
 | 
				
			||||||
sebserver.webservice.api.pagination.maxPageSize=500
 | 
					sebserver.webservice.api.pagination.maxPageSize=500
 | 
				
			||||||
# comma separated list of known possible OpenEdX API access token request endpoints
 | 
					# comma separated list of known possible OpenEdX API access token request endpoints
 | 
				
			||||||
sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
 | 
					sebserver.webservice.lms.openedx.api.token.request.paths=/oauth2/access_token
 | 
				
			||||||
 | 
					sebserver.webservice.lms.moodle.api.token.request.paths=
 | 
				
			||||||
sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias
 | 
					sebserver.webservice.lms.address.alias=lms.mockup.com=lms.address.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# NOTE: This is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin to
 | 
					# NOTE: This is a temporary work-around for SEB Restriction API within Open edX SEB integration plugin to
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
* {
 | 
					* {
 | 
				
			||||||
    color: #000000;
 | 
					    font: 12px Arial, Helvetica, sans-serif;
 | 
				
			||||||
    font: normal 12px Arial, Helvetica, sans-serif;
 | 
					    color: #4a4a4a;
 | 
				
			||||||
    background-image: none;
 | 
					    background-image: none;
 | 
				
			||||||
    background-color: #FFFFFF;
 | 
					    background-color: #FFFFFF;
 | 
				
			||||||
    padding: 0; 
 | 
					    padding: 0; 
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue