/** * 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.nio.file.Paths; import java.util.LinkedList; 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.Process; import com.rapidminer.RapidMiner; import com.rapidminer.RepositoryProcessLocation; import com.rapidminer.operator.FlagUserData; import com.rapidminer.repository.MalformedRepositoryLocationException; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; 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; import com.rapidminer.tools.XMLException; /** * A tutorial is a folder in the {@link TutorialGroup} zip file containing * * <ul> * <li>a {@code tutorial.properties} file which contains the default English properties</li> * <li>an arbitrarily number of localized {@code tutorial_xy.properties} files (e.g. * {@code tutorial_de.properties} with ISO-8859-1 encoding)</li> * <li>one {@code .rmp} process file</li> * <li>an arbitrarily number of {@code .blob} files * <p> * (a .blob file is a binary file which was imported into a local RapidMiner repository, e.g. a PNG * process background image)</li> * <li>an arbitrarily number of {@code .ioo} and {@code .md} files that are used in the process</li> * </ul> * The {@code tutorial.properties} file has to contain the keys * <ul> * <li>tutorial.name (which defines the title of the tutorial)</li> * <li>tutorial.description (which defines the description of the tutorial)</li> * </ul> * <strong>NOTE:</strong> <br/> * The content of the tutorial folder will be mirrored in the Sample Repository with the path * {@value #TUTORIALS_PATH} + {@link #name} + "/" + {@link #folder}. All resources which are used in * the tutorial process needs to adapt the resource location accordingly. * * @since 7.0.0 * @author Gisa Schaefer, Marcel Michel * @see TutorialGroup Description of the .tutorial file contents */ public class Tutorial implements ZipStreamResource { /** * User data key to flag processes as tutorial process: If the root operator's entry for this * key is non-null, the process is considered a tutorial. */ public static final String KEY_USER_DATA_FLAG = "com.rapidminer.tutorial.Tutorial"; /** key of the description in property files */ private static final String KEY_TUTORIAL_DESCRIPTION = "tutorial.description"; /** key of the title in property files */ private static final String KEY_TUTORIAL_NAME = "tutorial.name"; /** the repository location for tutorials */ private static final String TUTORIALS_PATH = "//Samples/Tutorials/"; /** the file name of the step file */ private static final String STEPS_XML = "steps.xml"; private static final String STEPS_XML_TEMPLATE = "steps_%s.xml"; /** the resources location for tutorials */ private static final String RESOURCES_LOCATION = "tutorial/"; private static final String NO_DESCRIPTION = I18N.getGUILabel("tutorial.no_description"); private static final String NO_TITLE = I18N.getGUILabel("tutorial.no_title"); private static final String DEFAULT_PROPERTY_FILE = "tutorial.properties"; private static final String PROPERTY_FILE_TEMPLATE = "tutorial_%s.properties"; private String title = NO_TITLE; private String description = NO_DESCRIPTION; private String processName; private List<String> demoData; private final Path path; private final TutorialGroup group; private final String folder; Tutorial(TutorialGroup group, String folder) throws IOException, RepositoryException { this(group, null, folder); } Tutorial(TutorialGroup group, Path path, String folder) throws IOException, RepositoryException { this.group = group; this.path = path; this.folder = folder; 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 folder; } /** * @return the name of the process behind this tutorial */ public String getProcessName() { return processName; } /** * @return the list of demo data names */ public List<String> getDemoData() { return demoData; } /** * @return the identifier for this tutorial */ public String getIdentifier() { return getGroup().getName() + "-" + folder; } /** * @return the group which contains the tutorial */ public TutorialGroup getGroup() { return group; } /** * @return the steps.xml file as {@link InputStream}, or {@code null} */ public InputStream getSteps() throws IOException, RepositoryException { String localeStepsName = getStepsFileName(); boolean localeAvailable = false; // sadly we need to traverse the zip file two times, // because the localized steps file should always be preferred, but it must not be available try (InputStream rawIn = getInputStream(); ZipInputStream zip = new ZipInputStream(rawIn)) { ZipEntry entry; while ((entry = zip.getNextEntry()) != null) { if (entry.isDirectory() || !entry.getName().startsWith(folder) || entry.getName().replaceFirst("/", "").contains("/")) { continue; } String entryName = entry.getName(); if (localeStepsName.equals(entryName.replaceFirst(folder, ""))) { // the input stream will automatically be closed, return it later localeAvailable = true; break; } } } InputStream rawIn = getInputStream(); ZipInputStream zip = new ZipInputStream(rawIn); ZipEntry entry; while ((entry = zip.getNextEntry()) != null) { if (entry.isDirectory() || !entry.getName().startsWith(folder) || entry.getName().replaceFirst("/", "").contains("/")) { continue; } String entryName = entry.getName(); if (localeStepsName.equals(entryName.replaceFirst(folder, ""))) { return zip; } else if (!localeAvailable && STEPS_XML.equals(entryName.replaceFirst(folder, ""))) { return zip; } } return null; } /** * @return a new process that contains the tutorial process * @throws XMLException * @throws IOException * @throws MalformedRepositoryLocationException */ public Process makeProcess() throws IOException, XMLException, MalformedRepositoryLocationException { String processLocation = TUTORIALS_PATH + getGroup().getName() + "/" + folder + processName; RepositoryProcessLocation repoLocation = new RepositoryProcessLocation(new RepositoryLocation(processLocation)); Process newProcess = new Process(repoLocation.getRawXML()); newProcess.getRootOperator().setUserData(KEY_USER_DATA_FLAG, new FlagUserData()); return newProcess; } /** * Loads the content of the zip file. */ private void load() throws IOException, RepositoryException { try (InputStream rawIn = getInputStream()) { demoData = new LinkedList<>(); 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.isDirectory() || !entry.getName().startsWith(folder) || entry.getName().replaceFirst("/", "").contains("/")) { continue; } String entryName = entry.getName(); if (DEFAULT_PROPERTY_FILE.equals(entryName.replaceFirst(folder, ""))) { defaultProps.load(zip); } else if (localeFileName.equals(entryName.replaceFirst(folder, ""))) { localProps.load(zip); } else if (entryName.endsWith(".rmp")) { processName = Paths.get(entryName).getFileName().toString().split("\\.")[0]; } else if (entryName.endsWith(".ioo")) { demoData.add(Paths.get(entryName).getFileName().toString().split("\\.")[0]); } } // load title and description from default props title = defaultProps.getProperty(KEY_TUTORIAL_NAME, NO_TITLE); description = defaultProps.getProperty(KEY_TUTORIAL_DESCRIPTION, NO_DESCRIPTION); // exchange titel and description by locale prop if available if (!localProps.isEmpty()) { title = localProps.getProperty(KEY_TUTORIAL_NAME, title); description = localProps.getProperty(KEY_TUTORIAL_DESCRIPTION, description); } if (title.isEmpty()) { title = NO_TITLE; } if (description.isEmpty()) { description = NO_DESCRIPTION; } } finally { zip.close(); // noop ; to avoid compile time warning about resource leak zip.close2(); } } } /** * @return the {@link InputStream} to of this tutorial */ private InputStream getInputStream() throws IOException, RepositoryException { if (path != null) { return Files.newInputStream(path); } else { return Tools.getResourceInputStream(RESOURCES_LOCATION + getGroup().getName() + ".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()); } /** * @return the name of the localized step file (e.g. steps_de.xml) */ private String getStepsFileName() { String localeLanguage = ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_GENERAL_LOCALE_LANGUAGE); Locale locale = Locale.getDefault(); if (localeLanguage != null) { locale = new Locale(localeLanguage); } return String.format(STEPS_XML_TEMPLATE, locale.getLanguage()); } }