/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.tutorial; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import com.rapidminer.RapidMiner; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.resource.ZipStreamResource; import com.rapidminer.tools.I18N; import com.rapidminer.tools.NonClosingZipInputStream; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.Tools; /** * The tutorial group must be a .zip file renamed to .tutorial containing * * <ul> * <li>a {@code group.properties} file which contains the default English properties</li> * <li>an arbitrarily number of localized {@code group_xy.properties} files (e.g. * {@code group_de.properties}) with ISO-8859-1 encoding</li> * <li>an arbitrarily number of folders which are defined in the {@link Tutorial} class</li> * </ul> * Any {@code group.properties} file has to contain the keys * <ul> * <li>group.name (which defines the title of the tutorial group)</li> * <li>group.description (which defines the description of the tutorial group)</li> * </ul> * </br> * The tutorial group files can be added into .RapidMiner/tutorials/ or under tutorial/ in the * extension resources ( * {@code src/main/resources/com.rapidminer.resources.tutorial/[fileName].tutorial}). Tutorial files * in the resources have to be registered via {@link TutorialRegistry#register(String)} while the * files from the .RapidMiner folder are loaded automatically. * <p> * Tutorials placed in the .RapidMiner folder will be preferred over tutorials with the same name * registered via {@link TutorialRegistry#register(String)}. * <p> * <strong>NOTE:</strong> <br/> * The content of the tutorial group will be mirrored in the Sample Repository, see the * {@link Tutorial} documentation for the path definition. * * @author Gisa Schaefer, Marcel Michel * @since 7.0.0 * */ public class TutorialGroup implements ZipStreamResource { private static final Comparator<Tutorial> TUTORIAL_COMPARATOR = new Comparator<Tutorial>() { @Override public int compare(Tutorial t1, Tutorial t2) { String n1 = t1.getStreamPath(); String n2 = t1.getStreamPath(); if (n1 == null && n2 == null) { return 0; } if (n1 == null && n2 != null) { return -1; } if (n1 != null && n2 == null) { return +1; } return n1.compareTo(n2); } }; /** key of the description in property files */ private static final String KEY_GROUP_DESCRIPTION = "group.description"; /** key of the title in property files */ private static final String KEY_GROUP_NAME = "group.name"; /** the resources location for tutorials */ private static final String RESOURCES_LOCATION = "tutorial/"; private static final String NO_DESCRIPTION = I18N.getGUILabel("tutorial.group.no_description"); private static final String NO_TITLE = I18N.getGUILabel("tutorial.group.no_title"); private static final String DEFAULT_PROPERTY_FILE = "group.properties"; private static final String PROPERTY_FILE_TEMPLATE = "group_%s.properties"; private String title = NO_TITLE; private String description = NO_DESCRIPTION; private Path path; private List<Tutorial> tutorials; private String name; TutorialGroup(String name) throws IOException, RepositoryException { this.name = name; load(); } TutorialGroup(Path path) throws IOException, RepositoryException { this.name = path.getFileName().toString().replaceAll("\\.tutorial", ""); this.path = path; load(); } /** * @return the stream to load resources associated with this tutorial */ @Override public ZipInputStream getStream() throws IOException, RepositoryException { return new ZipInputStream(getInputStream()); } @Override public String getTitle() { return title; } @Override public String getDescription() { return description; } @Override public String getStreamPath() { return null; } /** * @return the name of the tutorial which is also the name of the zip file */ public String getName() { return name; } /** * @return the contained {@link Tutorial}s as {@link List} */ public List<Tutorial> getTutorials() { return Collections.unmodifiableList(tutorials); } /** * Uses the {@link TutorialManager} to query the number of completed {@link Tutorial}s. * * @return the number of completed {@link Tutorial}s */ public int getNumberOfCompletedTutorials() { int count = 0; for (Tutorial tutorial : tutorials) { if (TutorialManager.INSTANCE.hasCompletedTutorial(tutorial.getIdentifier())) { count++; } } return count; } /** * @return the number of contained {@link Tutorial}s */ public int getNumberOfTutorials() { return tutorials.size(); } /** * @return {@code true} if the result of {@link #getNumberOfTutorials()} is equal to the result * of {@link #getNumberOfCompletedTutorials()}, otherwise {@code false} */ public boolean hasCompleted() { return getNumberOfCompletedTutorials() == getNumberOfTutorials(); } /** * Loads the content of the zip file. */ private void load() throws IOException, RepositoryException { tutorials = new ArrayList<>(); try (InputStream rawIn = getInputStream()) { NonClosingZipInputStream zip = new NonClosingZipInputStream(rawIn); try { ZipEntry entry; String localeFileName = getPropertyFileName(); Properties defaultProps = new Properties(); Properties localProps = new Properties(); while ((entry = zip.getNextEntry()) != null) { if (entry.getName().replaceFirst("/", "").contains("/")) { // ignore second folder level and above continue; } String entryName = entry.getName(); if (entry.isDirectory()) { if (path != null) { tutorials.add(new Tutorial(this, path, entryName)); } else { tutorials.add(new Tutorial(this, entryName)); } } else if (DEFAULT_PROPERTY_FILE.equals(entryName)) { defaultProps.load(zip); } else if (localeFileName.equals(entryName)) { localProps.load(zip); } } // load title and description from default props title = defaultProps.getProperty(KEY_GROUP_NAME, NO_TITLE); description = defaultProps.getProperty(KEY_GROUP_DESCRIPTION, NO_DESCRIPTION); // exchange titel and description by locale prop if available if (!localProps.isEmpty()) { title = localProps.getProperty(KEY_GROUP_NAME, title); description = localProps.getProperty(KEY_GROUP_DESCRIPTION, description); } if (title.isEmpty()) { title = NO_TITLE; } if (description.isEmpty()) { description = NO_DESCRIPTION; } Collections.sort(tutorials, TUTORIAL_COMPARATOR); } finally { zip.close(); // noop ; to avoid compile time warning about resource leak zip.close2(); } } } /** * @return the {@link InputStream} to of this tutorial group */ private InputStream getInputStream() throws IOException, RepositoryException { if (path != null) { return Files.newInputStream(path); } else { return Tools.getResourceInputStream(RESOURCES_LOCATION + name + ".tutorial"); } } /** * @return the name of the localized property file (e.g. tutorial_de.properties) */ private String getPropertyFileName() { String localeLanguage = ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_GENERAL_LOCALE_LANGUAGE); Locale locale = Locale.getDefault(); if (localeLanguage != null) { locale = new Locale(localeLanguage); } return String.format(PROPERTY_FILE_TEMPLATE, locale.getLanguage()); } }