Example #1
0
/**
 * Initial date: 07.04.2015<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
@Service
public class QTIPreWarm implements PreWarm {

  private static final OLog log = Tracing.createLoggerFor(QTIPreWarm.class);

  @Override
  public void run() {
    long start = System.nanoTime();
    log.info("Start scanning for QTI resources");

    List<String> types = new ArrayList<>(2);
    types.add(TestFileResource.TYPE_NAME);
    types.add(SurveyFileResource.TYPE_NAME);
    List<OLATResource> qtiResources =
        CoreSpringFactory.getImpl(OLATResourceManager.class).findResourceByTypes(types);
    DBFactory.getInstance().commitAndCloseSession();
    for (OLATResource qtiResource : qtiResources) {
      OnyxModule.isOnyxTest(qtiResource);
    }
    log.info(
        qtiResources.size()
            + " QTI Resources scanned in (ms): "
            + CodeHelper.nanoToMilliTime(start));
  }
}
/**
 * Initial date: 22.03.2013<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
public class HtmlStaticMapper implements Mapper {

  private static final OLog log = Tracing.createLoggerFor(HtmlStaticMapper.class);

  private VFSContainer mapperRootContainer;

  public HtmlStaticMapper() {
    // for serializable/deserializable
  }

  public HtmlStaticMapper(VFSContainer mapperRootContainer) {
    this.mapperRootContainer = mapperRootContainer;
  }

  public MediaResource handle(String relPath, HttpServletRequest request) {
    if (log.isDebug()) log.debug("CPComponent Mapper relPath=" + relPath);

    VFSItem currentItem = mapperRootContainer.resolve(relPath);
    if (currentItem == null || (currentItem instanceof VFSContainer)) {
      return new NotFoundMediaResource(relPath);
    }
    VFSMediaResource vmr = new VFSMediaResource((VFSLeaf) currentItem);
    String encoding = SimpleHtmlParser.extractHTMLCharset(((VFSLeaf) currentItem));
    if (log.isDebug()) log.debug("CPComponent Mapper set encoding=" + encoding);
    vmr.setEncoding(encoding); //
    return vmr;
  }
}
 /**
  * It is assumed that if the caller of this method is allowed to see the forum thread starting
  * from topMessageId, then he also has the right to archive it, so no need for a ForumCallback.
  *
  * @param forumFormatter
  * @param forumId
  * @param topMessageId
  * @return the message thread as String formatted
  */
 public String applyFormatterForOneThread(
     final ForumFormatter forumFormatter, final long forumId, final long topMessageId) {
   Tracing.logInfo(
       "Archiving forum.thread: " + forumId + "." + topMessageId, ForumArchiveManager.class);
   final Map metaInfo = new HashMap();
   metaInfo.put(ForumFormatter.MANDATORY_METAINFO_KEY, new Long(forumId));
   final MessageNode topMessageNode = convertToThreadTree(topMessageId);
   return formatThread(topMessageNode, forumFormatter, metaInfo);
 }
 /**
  * If the forumCallback is null no restriction applies to the forum archiver. (that is it can
  * archive all threads no matter the status)
  *
  * @param forumFormatter
  * @param forumId
  * @param forumCallback
  * @return
  */
 public String applyFormatter(
     final ForumFormatter forumFormatter, final long forumId, final ForumCallback forumCallback) {
   Tracing.logInfo("Archiving complete forum: " + forumId, ForumArchiveManager.class);
   final Map metaInfo = new HashMap();
   metaInfo.put(ForumFormatter.MANDATORY_METAINFO_KEY, new Long(forumId));
   // convert forum structure to trees
   final List threadTreesList = convertToThreadTrees(forumId, forumCallback);
   // format forum trees by using the formatter given by the callee
   return formatForum(threadTreesList, forumFormatter, metaInfo);
 }
  /**
   * @see org.olat.course.nodes.GenericCourseNode#archiveNodeData(java.util.Locale,
   *     org.olat.course.ICourse, java.io.File, java.lang.String)
   */
  @Override
  public boolean archiveNodeData(
      final Locale locale, final ICourse course, final File exportDirectory, final String charset) {
    final String repoRef = (String) this.getModuleConfiguration().get("reporef");
    final OLATResourceable ores =
        RepositoryManager.getInstance()
            .lookupRepositoryEntryBySoftkey(repoRef, true)
            .getOlatResource();

    if (WikiManager.getInstance().getOrLoadWiki(ores).getAllPagesWithContent().size() > 0) {
      // OK, there is somthing to archive
      final VFSContainer exportContainer = new LocalFolderImpl(exportDirectory);
      VFSContainer wikiExportContainer =
          (VFSContainer) exportContainer.resolve(WikiManager.WIKI_RESOURCE_FOLDER_NAME);
      if (wikiExportContainer == null) {
        wikiExportContainer =
            exportContainer.createChildContainer(WikiManager.WIKI_RESOURCE_FOLDER_NAME);
      }
      final String exportDirName =
          getShortTitle()
              + "_"
              + Formatter.formatDatetimeFilesystemSave(new Date(System.currentTimeMillis()));
      final VFSContainer destination = wikiExportContainer.createChildContainer(exportDirName);
      if (destination == null) {
        Tracing.logError(
            "archiveNodeData: Could not create destination directory: wikiExportContainer="
                + wikiExportContainer
                + ", exportDirName="
                + exportDirName,
            getClass());
      }

      final VFSContainer container =
          WikiManager.getInstance().getWikiContainer(ores, WikiManager.WIKI_RESOURCE_FOLDER_NAME);
      if (container
          != null) { // the container could be null if the wiki is an old empty one - so nothing to
                     // archive
        final VFSContainer parent = container.getParentContainer();
        final VFSLeaf wikiZip = WikiToZipUtils.getWikiAsZip(parent);
        destination.copyFrom(wikiZip);
      }
      return true;
    }
    // empty wiki, no need to archive
    return false;
  }
/**
 * Description:<br>
 * Notification handler for course node task. Subscribers get informed about new uploaded file in
 * the solution folder.
 *
 * <p>Initial Date: 23.06.2010 <br>
 *
 * @author christian guretzki
 */
public class SolutionFileUploadNotificationHandler extends AbstractTaskNotificationHandler
    implements NotificationsHandler {
  private static final String CSS_CLASS_SOLUTION_ICON = "o_solution_icon";
  private static OLog log = Tracing.createLoggerFor(SolutionFileUploadNotificationHandler.class);

  public SolutionFileUploadNotificationHandler() {
    // empty block
  }

  protected static SubscriptionContext getSubscriptionContext(
      UserCourseEnvironment userCourseEnv, CourseNode node) {
    return CourseModule.createSubscriptionContext(
        userCourseEnv.getCourseEnvironment(), node, "Solutionbox");
  }

  protected String getCssClassIcon() {
    return CSS_CLASS_SOLUTION_ICON;
  }

  protected String getNotificationHeaderKey() {
    return "solution.notifications.header";
  }

  protected String getNotificationEntryKey() {
    return "solution.notifications.entry";
  }

  protected OLog getLogger() {
    return log;
  }

  @Override
  public String getType() {
    return "SolutionController";
  }
}
Example #7
0
public class VFSManager {
  private static final OLog log = Tracing.createLoggerFor(VFSManager.class);

  /**
   * Make sure we always have a path that starts with a "/".
   *
   * @param path
   * @return
   */
  public static String sanitizePath(String path) {
    // check for "empty" paths
    if (path == null || path.length() == 0) return "/";
    // prepend "/" if missing
    if (path.charAt(0) != '/') path = "/" + path;
    // cut trailing slash if any
    if (path.length() > 1 && path.charAt(path.length() - 1) == '/')
      path = path.substring(0, path.length() - 1);
    return path;
  }

  /**
   * Extract the next subfolder (e.g. /foo/bla/gnu.txt -> "foo" PRE: a sanitized path, has a child
   *
   * @param path
   * @return Next child.
   */
  public static String extractChild(String path) {
    int slPos = path.indexOf('/', 1);
    String childName = null;
    if (slPos == -1) { // no subpath
      childName = path.substring(1);
    } else {
      childName = path.substring(1, slPos);
    }
    return childName;
  }

  /**
   * Check if descendant is indeed a descendant of root..
   *
   * @param parent
   * @param child
   * @return
   */
  public static boolean isContainerDescendantOrSelf(VFSContainer descendant, VFSContainer root) {
    if (root.isSame(descendant)) return true;
    VFSContainer parentContainer = descendant.getParentContainer();
    while (parentContainer != null) {
      if (parentContainer.isSame(root)) return true;
      parentContainer = parentContainer.getParentContainer();
    }
    return false;
  }

  /**
   * Check if descendant is child of parent or same as parent.
   *
   * @param descendant
   * @param root
   * @return
   */
  public static boolean isSelfOrParent(VFSContainer descendant, VFSContainer parent) {
    if (parent.isSame(descendant)) return true;
    VFSContainer parentContainer = descendant.getParentContainer();
    if (parentContainer != null && parentContainer.isSame(parent)) return true;

    return false;
  }

  public static boolean isDirectoryAndNotEmpty(VFSItem directory) {
    if (directory instanceof VFSContainer) {
      List<VFSItem> children = ((VFSContainer) directory).getItems();
      return !children.isEmpty();
    }
    return false;
  }

  /** @see org.olat.core.util.vfs.VFSItem#resolveFile(java.lang.String) */
  public static VFSItem resolveFile(VFSContainer rootContainer, String path) {

    path = VFSManager.sanitizePath(path);
    if (path.equals("/")) { // slash or empty path -> return this vfsitem
      return rootContainer;
    }

    // The following code block eliminates directory scans on file-systems,
    // which are done in the original code in the next block, which is left
    // there as a fall-back in case there are non-file-system implementations
    // of OLAT-VFS.
    // OLAT file-systems can be very large and directories can contain
    // quite numerous files. Scanning these can take a lot of time.
    // Just put together the paths of both arguments
    // and ask the file-system whether such an entry
    // exists. If yes, this entry must be exactly what is
    // to be returned as, the proper type of, VFSItem.
    if (rootContainer instanceof LocalFolderImpl) {
      String childName = extractChild(path);
      LocalFolderImpl l = (LocalFolderImpl) rootContainer;
      File t = new File(l.getBasefile().getAbsolutePath(), childName);
      if (t.exists()) {
        String bcroot = FolderConfig.getCanonicalRoot();
        String fsPath = t.getAbsolutePath();
        if (t.isDirectory()) {
          VFSContainer subContainer;
          if (fsPath.startsWith(bcroot)) {
            fsPath = fsPath.substring(bcroot.length(), fsPath.length());
            subContainer = new OlatRootFolderImpl(fsPath, rootContainer);
          } else {
            subContainer = new LocalFolderImpl(t, rootContainer);
          }
          String subPath = path.substring(childName.length() + 1);
          return resolveFile(subContainer, subPath);
        } else {
          if (fsPath.startsWith(bcroot)) {
            fsPath = fsPath.replace(bcroot, "");
            return new OlatRootFileImpl(fsPath, rootContainer);
          } else {
            return new LocalFileImpl(t, rootContainer);
          }
        }
      } else {
        return null;
      }
    }

    // leave original code block as fall-back for non-file-system-based implementations
    String childName = extractChild(path);
    List<VFSItem> children = rootContainer.getItems();
    for (VFSItem child : children) {
      String curName = child.getName();
      if (childName.equals(curName)) { // found , let child further resolve if needed
        return child.resolve(path.substring(childName.length() + 1));
      }
    }
    return null;
  }

  /**
   * Resolves a directory path in the base container or creates this directory. The method creates
   * any missing directories.
   *
   * @param baseContainer The base directory. User must have write permissions on this container
   * @param relContainerPath The path relative to the base container. Must start with a '/'. To
   *     separate sub directories use '/'
   * @return The resolved or created container or NULL if a problem happened
   */
  public static VFSContainer resolveOrCreateContainerFromPath(
      VFSContainer baseContainer, String relContainerPath) {
    VFSContainer resultContainer = baseContainer;
    if (!VFSConstants.YES.equals(baseContainer.canWrite())) {
      log.error(
          "Could not create relPath::"
              + relContainerPath
              + ", base container::"
              + getRealPath(baseContainer)
              + " not writable",
          null);
      resultContainer = null;
    } else if (StringHelper.containsNonWhitespace(relContainerPath)) {
      // Try to resolve given rel path from current container
      VFSItem resolvedPath = baseContainer.resolve(relContainerPath.trim());
      if (resolvedPath == null) {
        // Does not yet exist - create subdir
        String[] pathSegments = relContainerPath.split("/");
        for (int i = 0; i < pathSegments.length; i++) {
          String segment = pathSegments[i].trim();
          if (StringHelper.containsNonWhitespace(segment)) {
            resolvedPath = resultContainer.resolve(segment);
            if (resolvedPath == null) {
              resultContainer = resultContainer.createChildContainer(segment);
              if (resultContainer == null) {
                log.error(
                    "Could not create container with name::"
                        + segment
                        + " in relPath::"
                        + relContainerPath
                        + " in base container::"
                        + getRealPath(baseContainer),
                    null);
                break;
              }
            } else {
              if (resolvedPath instanceof VFSContainer) {
                resultContainer = (VFSContainer) resolvedPath;
              } else {
                resultContainer = null;
                log.error(
                    "Could not create container with name::"
                        + segment
                        + " in relPath::"
                        + relContainerPath
                        + ", a file with this name exists (but not a directory) in base container::"
                        + getRealPath(baseContainer),
                    null);
                break;
              }
            }
          }
        }
      } else {
        // Parent dir already exists,  make sure this is really a container and not a file!
        if (resolvedPath instanceof VFSContainer) {
          resultContainer = (VFSContainer) resolvedPath;
        } else {
          resultContainer = null;
          log.error(
              "Could not create relPath::"
                  + relContainerPath
                  + ", a file with this name exists (but not a directory) in base container::"
                  + getRealPath(baseContainer),
              null);
        }
      }
    }
    return resultContainer;
  }

  /**
   * Resolves a file path in the base container or creates this file under the given path. The
   * method creates any missing directories.
   *
   * @param baseContainer The base directory. User must have write permissions on this container
   * @param relFilePath The path relative to the base container. Must start with a '/'. To separate
   *     sub directories use '/'
   * @return The resolved or created leaf or NULL if a problem happened
   */
  public static VFSLeaf resolveOrCreateLeafFromPath(
      VFSContainer baseContainer, String relFilePath) {
    if (StringHelper.containsNonWhitespace(relFilePath)) {
      int lastSlash = relFilePath.lastIndexOf("/");
      String relDirPath = relFilePath;
      String fileName = null;
      if (lastSlash == -1) {
        // relFilePath is the file name - no directories involved
        relDirPath = null;
        fileName = relFilePath;
      } else if (lastSlash == 0) {
        // Remove start slash from file name
        relDirPath = null;
        fileName = relFilePath.substring(1, relFilePath.length());
      } else {
        relDirPath = relFilePath.substring(0, lastSlash);
        fileName = relFilePath.substring(lastSlash);
      }

      // Create missing directories and set parent dir for later file creation
      VFSContainer parent = baseContainer;
      if (StringHelper.containsNonWhitespace(relDirPath)) {
        parent = resolveOrCreateContainerFromPath(baseContainer, relDirPath);
      }
      // Now create file in that dir
      if (StringHelper.containsNonWhitespace(fileName)) {
        VFSLeaf leaf = null;
        VFSItem resolvedFile = parent.resolve(fileName);
        if (resolvedFile == null) {
          leaf = parent.createChildLeaf(fileName);
          if (leaf == null) {
            log.error(
                "Could not create leaf with relPath::"
                    + relFilePath
                    + " in base container::"
                    + getRealPath(baseContainer),
                null);
          }
        } else {
          if (resolvedFile instanceof VFSLeaf) {
            leaf = (VFSLeaf) resolvedFile;
          } else {
            leaf = null;
            log.error(
                "Could not create relPath::"
                    + relFilePath
                    + ", a directory with this name exists (but not a file) in base container::"
                    + getRealPath(baseContainer),
                null);
          }
        }
        return leaf;
      }
    }
    return null;
  }

  /**
   * Get the security callback which affects this item. This searches up the path of parents to see
   * wether it can find any callback. If no callback can be found, null is returned.
   *
   * @param vfsItem
   * @return
   */
  public static VFSSecurityCallback findInheritedSecurityCallback(VFSItem vfsItem) {
    VFSItem inheritingItem = findInheritingSecurityCallbackContainer(vfsItem);
    if (inheritingItem != null) return inheritingItem.getLocalSecurityCallback();
    return null;
  }

  /**
   * Get the container which security callback affects this item. This searches up the path of
   * parents to see wether it can find any container with a callback. If no callback can be found,
   * null is returned.
   *
   * @param vfsItem
   * @return
   */
  public static VFSContainer findInheritingSecurityCallbackContainer(VFSItem vfsItem) {
    if (vfsItem == null) return null;
    // first resolve delegates of any NamedContainers to get the actual container (might be a
    // MergeSource)
    if (vfsItem instanceof NamedContainerImpl)
      return findInheritingSecurityCallbackContainer(((NamedContainerImpl) vfsItem).getDelegate());
    // special treatment for MergeSource
    if (vfsItem instanceof MergeSource) {
      MergeSource mergeSource = (MergeSource) vfsItem;
      VFSContainer rootWriteContainer = mergeSource.getRootWriteContainer();
      if (rootWriteContainer != null && rootWriteContainer.getLocalSecurityCallback() != null) {
        // if the root write container has a security callback set, it will always override
        // any local securitycallback set on the mergesource
        return rootWriteContainer;
      } else if (mergeSource.getLocalSecurityCallback() != null) {
        return mergeSource;
      } else if (mergeSource.getParentContainer() != null) {
        return findInheritingSecurityCallbackContainer(mergeSource.getParentContainer());
      }
    } else {
      if ((vfsItem instanceof VFSContainer) && (vfsItem.getLocalSecurityCallback() != null))
        return (VFSContainer) vfsItem;
      if (vfsItem.getParentContainer() != null)
        return findInheritingSecurityCallbackContainer(vfsItem.getParentContainer());
    }
    return null;
  }

  /**
   * Check wether this container has a quota assigned to itself.
   *
   * @param container
   * @return Quota if this container has a Quota assigned, null otherwise.
   */
  public static Quota isTopLevelQuotaContainer(VFSContainer container) {
    VFSSecurityCallback callback = container.getLocalSecurityCallback();
    if (callback != null && callback.getQuota() != null) return callback.getQuota();

    // extract delegate if this is a NamedContainer instance...
    if (container instanceof NamedContainerImpl)
      container = ((NamedContainerImpl) container).getDelegate();

    // check if this is a MergeSource with a root write container
    if (container instanceof MergeSource) {
      VFSContainer rwContainer = ((MergeSource) container).getRootWriteContainer();
      if (rwContainer != null
          && rwContainer.getLocalSecurityCallback() != null
          && rwContainer.getLocalSecurityCallback().getQuota() != null)
        return rwContainer.getLocalSecurityCallback().getQuota();
    }
    return null;
  }

  /**
   * Check the quota usage on this VFSContainer. If no security callback is provided, this returns
   * -1 (meaning no quota on this folder). Similarly, if no quota is defined,
   * VFSSecurityCallback.NO_QUOTA_DEFINED will be returned to signal no quota on this container.
   *
   * @param securityCallback
   * @param container
   * @return
   */
  public static long getQuotaLeftKB(VFSContainer container) {
    VFSContainer inheritingItem = findInheritingSecurityCallbackContainer(container);
    if (inheritingItem == null || inheritingItem.getLocalSecurityCallback().getQuota() == null)
      return Quota.UNLIMITED;
    long usageKB = getUsageKB(inheritingItem);
    return inheritingItem.getLocalSecurityCallback().getQuota().getQuotaKB().longValue() - usageKB;
  }

  /**
   * Recursively traverse the container and sum up all leafs' sizes.
   *
   * @param container
   * @return
   */
  public static long getUsageKB(VFSItem vfsItem) {
    if (vfsItem instanceof VFSContainer) {
      // VFSContainer
      if (vfsItem instanceof LocalFolderImpl)
        return FileUtils.getDirSize(((LocalFolderImpl) vfsItem).getBasefile()) / 1024;
      long usageKB = 0;
      List<VFSItem> children = ((VFSContainer) vfsItem).getItems();
      for (VFSItem child : children) {
        usageKB += getUsageKB(child);
      }
      return usageKB;
    } else {
      // VFSLeaf
      return ((VFSLeaf) vfsItem).getSize() / 1024;
    }
  }

  /**
   * Returns the real path of the given VFS container. If the container is a named container, the
   * delegate container is used. If the container is a merge source with a writable root container,
   * then this one is used. In other cases the method returns null since the given container is not
   * writable to any real file.
   *
   * @param container
   * @return String representing an absolute path for this container
   */
  public static String getRealPath(VFSContainer container) {
    File file = getRealFile(container);
    if (file == null) return null;
    return file.getPath();
  }

  public static File getRealFile(VFSContainer container) {
    File realFile = null;
    LocalFolderImpl localFolder = null;
    if (container instanceof NamedContainerImpl) {
      container = ((NamedContainerImpl) container).getDelegate();
    }
    if (container instanceof MergeSource) {
      container = ((MergeSource) container).getRootWriteContainer();
    }
    if (container != null && container instanceof LocalFolderImpl) {
      localFolder = (LocalFolderImpl) container;
      realFile = localFolder.getBasefile();
    }
    return realFile;
  }

  /**
   * Get the path as string of the given item relative to the root container and the relative base
   * path
   *
   * @param item the item for which the relative path should be returned
   * @param rootContainer The root container for which the relative path should be calculated
   * @param relativeBasePath when NULL, the path will be calculated relative to the rootContainer;
   *     when NOT NULL, the relativeBasePath must represent a relative path within the root
   *     container that serves as the base. In this case, the calculated relative item path will
   *     start from this relativeBasePath
   * @return
   */
  public static String getRelativeItemPath(
      VFSItem item, VFSContainer rootContainer, String relativeBasePath) {
    // 1) Create path absolute to the root container
    if (item == null) return null;
    String absPath = "";
    VFSItem tmpItem = item;
    // Check for merged containers to fix problems with named containers, see OLAT-3848
    List<NamedContainerImpl> namedRootChilds = new ArrayList<NamedContainerImpl>();
    for (VFSItem rootItem : rootContainer.getItems()) {
      if (rootItem instanceof NamedContainerImpl) {
        namedRootChilds.add((NamedContainerImpl) rootItem);
      }
    }
    // Check if root container is the same as the item and vice versa. It is
    // necessary to perform the check on both containers to catch all potential
    // cases with MergedSource and NamedContainer where the check in one
    // direction is not necessarily the same as the opposite check
    while (tmpItem != null && !rootContainer.isSame(tmpItem) && !tmpItem.isSame(rootContainer)) {
      String itemFileName = tmpItem.getName();
      // fxdiff FXOLAT-125: virtual file system for CP
      if (tmpItem instanceof NamedLeaf) {
        itemFileName = ((NamedLeaf) tmpItem).getDelegate().getName();
      }

      // Special case: check if this is a named container, see OLAT-3848
      for (NamedContainerImpl namedRootChild : namedRootChilds) {
        if (namedRootChild.isSame(tmpItem)) {
          itemFileName = namedRootChild.getName();
        }
      }
      absPath = "/" + itemFileName + absPath;
      tmpItem = tmpItem.getParentContainer();
      if (tmpItem != null) {
        // test if this this is a merge source child container, see OLAT-5726
        VFSContainer grandParent = tmpItem.getParentContainer();
        if (grandParent instanceof MergeSource) {
          MergeSource mergeGrandParent = (MergeSource) grandParent;
          if (mergeGrandParent.isContainersChild((VFSContainer) tmpItem)) {
            // skip this parent container and use the merge grand-parent
            // instead, otherwise path contains the container twice
            tmpItem = mergeGrandParent;
          }
        }
      }
    }

    if (relativeBasePath == null) {
      return absPath;
    }
    // 2) Compute rel path to base dir of the current file

    // selpath = /a/irwas/subsub/nochsub/note.html 5
    // filenam = /a/irwas/index.html 3
    // --> subsub/nochsub/note.gif

    // or /a/irwas/bla/index.html
    // to /a/other/b/gugus.gif
    // --> ../../ other/b/gugus.gif

    // or /a/other/b/main.html
    // to /a/irwas/bla/goto.html
    // --> ../../ other/b/gugus.gif

    String base = relativeBasePath; // assume "/" is here
    if (!(base.indexOf("/") == 0)) {
      base = "/" + base;
    }

    String[] baseA = base.split("/");
    String[] targetA = absPath.split("/");
    int sp = 1;
    for (; sp < Math.min(baseA.length, targetA.length); sp++) {
      if (!baseA[sp].equals(targetA[sp])) {
        break;
      }
    }
    // special case: self-reference
    if (absPath.equals(base)) {
      sp = 1;
    }
    StringBuilder buffer = new StringBuilder();
    for (int i = sp; i < baseA.length - 1; i++) {
      buffer.append("../");
    }
    for (int i = sp; i < targetA.length; i++) {
      buffer.append(targetA[i] + "/");
    }
    buffer.deleteCharAt(buffer.length() - 1);
    String path = buffer.toString();

    String trimmed = path; // selectedPath.substring(1);
    return trimmed;
  }

  /**
   * This method takes a VFSContainer and a relative path to a file that exists within this
   * container. The method checks if the given container is a writable container that can be used
   * e.g. by the HTML editor as a base directory where to store some things. If the method detects
   * that this is not the case it works against the relative file path and checks each directory in
   * the path. <br>
   * The result will be an object array that contains the corrected container and the new relative
   * path. If no writable container could be found NULL is returned. <br>
   * Limitations: the method stops at least after 20 iterations returning NULL
   *
   * @param rootDir the container that should be checked
   * @param relFilePath The valid file path within this container
   * @return Object array that contains 1) a writable rootDir and 2) the corrected relFilePath that
   *     mathes to the new rootDir. Can be NULL if no writable root folder could be found.
   */
  public static ContainerAndFile findWritableRootFolderFor(
      VFSContainer rootDir, String relFilePath) {
    int level = 0;
    return findWritableRootFolderForRecursion(rootDir, relFilePath, level);
  }

  private static ContainerAndFile findWritableRootFolderForRecursion(
      VFSContainer rootDir, String relFilePath, int recursionLevel) {
    recursionLevel++;
    if (recursionLevel > 20) {
      // Emergency exit condition: a directory hierarchy that has more than 20
      // levels? Probably not..
      log.warn(
          "Reached recursion level while finding writable root Folder - most likely a bug. rootDir::"
              + rootDir
              + " relFilePath::"
              + relFilePath);
      return null;
    }

    if (rootDir instanceof NamedContainerImpl) {
      rootDir = ((NamedContainerImpl) rootDir).getDelegate();
    }
    if (rootDir instanceof MergeSource) {
      MergeSource mergedDir = (MergeSource) rootDir;
      // first check if the next level is not a second MergeSource
      int stop = relFilePath.indexOf("/", 1);
      if (stop > 0) {
        String nextLevel = extractChild(relFilePath);
        VFSItem item = mergedDir.resolve(nextLevel);
        if (item instanceof NamedContainerImpl) {
          item = ((NamedContainerImpl) item).getDelegate();
        }
        if (item instanceof MergeSource) {
          rootDir = (MergeSource) item;
          relFilePath = relFilePath.substring(stop);
          return findWritableRootFolderForRecursion(rootDir, relFilePath, recursionLevel);
        }
      }

      VFSContainer rootWriteContainer = mergedDir.getRootWriteContainer();
      if (rootWriteContainer == null) {
        // we have a merge source without a write container, try it one higher,
        // go through all children of this one and search the correct child in
        // the path
        List<VFSItem> children = rootDir.getItems();
        if (children.isEmpty()) {
          // ups, a merge source without children, no good, return null
          return null;
        }

        String nextChildName = relFilePath.substring(1, relFilePath.indexOf("/", 1));
        for (VFSItem child : children) {
          // look up for the next child in the path
          if (child.getName().equals(nextChildName)) {
            // use this child as new root and remove the child name from the rel
            // path
            if (child instanceof VFSContainer) {
              rootDir = (VFSContainer) child;
              relFilePath = relFilePath.substring(relFilePath.indexOf("/", 1));
              break;
            } else {
              // ups, a merge source with a child that is not a VFSContainer -
              // no good, return null
              return null;
            }
          }
        }
      } else {
        // ok, we found a merge source with a write container
        rootDir = rootWriteContainer;
      }
    }
    if (rootDir != null && rootDir instanceof LocalFolderImpl) {
      // finished, we found a local folder we can use to write
      return new ContainerAndFile(rootDir, relFilePath);
    } else {
      // do recursion
      return findWritableRootFolderForRecursion(rootDir, relFilePath, recursionLevel);
    }
  }

  /**
   * Returns a similar but non existing file name in root based on the given name.
   *
   * @param root
   * @param name
   * @return A non existing name based on the given name in the root directory
   */
  public static String similarButNonExistingName(VFSContainer root, String name) {
    VFSItem existingItem = null;
    String newName = name;
    existingItem = root.resolve(newName);
    int i = 1;
    while (existingItem != null) {
      newName = FileUtils.appendNumberAtTheEndOfFilename(name, i++);
      existingItem = root.resolve(newName);
    }
    return newName;
  }

  public static VFSContainer getOrCreateContainer(VFSContainer parent, String name) {
    VFSItem item = parent.resolve(name);
    if (item instanceof VFSContainer) {
      return (VFSContainer) item;
    } else if (item != null) {
      return null; // problem
    } else {
      return parent.createChildContainer(name);
    }
  }

  /**
   * Copies the content of the source to the target leaf.
   *
   * @param source
   * @param target
   * @return True on success, false on failure
   */
  public static boolean copyContent(VFSLeaf source, VFSLeaf target) {
    boolean successful;
    if (source != null && target != null) {
      InputStream in = new BufferedInputStream(source.getInputStream());
      OutputStream out = new BufferedOutputStream(target.getOutputStream(false));
      // write the input to the output
      try {
        byte[] buf = new byte[FileUtils.BSIZE];
        int i = 0;
        while ((i = in.read(buf)) != -1) {
          out.write(buf, 0, i);
        }
        successful = true;
      } catch (IOException e) {
        // something went wrong.
        successful = false;
        log.error(
            "Error while copying content from source: "
                + source.getName()
                + " to target: "
                + target.getName(),
            e);
      } finally {
        // Close streams
        try {
          if (out != null) {
            out.flush();
            out.close();
          }
          if (in != null) {
            in.close();
          }
        } catch (IOException ex) {
          log.error("Error while closing/cleaning up in- and output streams", ex);
        }
      }
    } else {
      // source or target is null
      successful = false;
      if (log.isDebug())
        log.debug("Either the source or the target is null. Content of leaf cannot be copied.");
    }
    return successful;
  }

  /**
   * Copy the content of the source container to the target container.
   *
   * @param source
   * @param target
   * @return
   */
  public static boolean copyContent(VFSContainer source, VFSContainer target) {
    if (!source.exists()) {
      return false;
    }
    if (isSelfOrParent(source, target)) {
      return false;
    }

    if (source instanceof NamedContainerImpl) {
      source = ((NamedContainerImpl) source).getDelegate();
    }
    if (target instanceof NamedContainerImpl) {
      target = ((NamedContainerImpl) target).getDelegate();
    }

    if (source instanceof LocalImpl && target instanceof LocalImpl) {
      LocalImpl localSource = (LocalImpl) source;
      LocalImpl localTarget = (LocalImpl) target;
      File localSourceFile = localSource.getBasefile();
      File localTargetFile = localTarget.getBasefile();
      return FileUtils.copyDirContentsToDir(localSourceFile, localTargetFile, false, "VFScopyDir");
    }
    return false;
  }

  /**
   * Copy the content of the file in the target leaf.
   *
   * @param source A file
   * @param target The target leaf
   * @return
   */
  public static boolean copyContent(File source, VFSLeaf target) {
    try (InputStream inStream = new FileInputStream(source)) {
      return copyContent(inStream, target, true);
    } catch (IOException ex) {
      log.error("", ex);
      return false;
    }
  }

  /**
   * Copies the stream to the target leaf.
   *
   * @param source
   * @param target
   * @return True on success, false on failure
   */
  public static boolean copyContent(InputStream inStream, VFSLeaf target) {
    return copyContent(inStream, target, true);
  }

  /**
   * Copies the stream to the target leaf.
   *
   * @param source
   * @param target
   * @param closeInput set to false if it's a ZipInputStream
   * @return True on success, false on failure
   */
  public static boolean copyContent(InputStream inStream, VFSLeaf target, boolean closeInput) {
    boolean successful;
    if (inStream != null && target != null) {
      InputStream in = new BufferedInputStream(inStream);
      OutputStream out = new BufferedOutputStream(target.getOutputStream(false));
      // write the input to the output
      try {
        byte[] buf = new byte[FileUtils.BSIZE];
        int i = 0;
        while ((i = in.read(buf)) != -1) {
          out.write(buf, 0, i);
        }
        successful = true;
      } catch (IOException e) {
        // something went wrong.
        successful = false;
        log.error(
            "Error while copying content from source: "
                + inStream
                + " to target: "
                + target.getName(),
            e);
      } finally {
        // Close streams
        try {
          if (out != null) {
            out.flush();
            out.close();
          }
          if (closeInput && in != null) {
            in.close();
          }
        } catch (IOException ex) {
          log.error("Error while closing/cleaning up in- and output streams", ex);
        }
      }
    } else {
      // source or target is null
      successful = false;
      if (log.isDebug())
        log.debug("Either the source or the target is null. Content of leaf cannot be copied.");
    }
    return successful;
  }

  /**
   * @param container
   * @param filename
   * @return
   */
  public static String rename(VFSContainer container, String filename) {
    String newName = filename;
    VFSItem newFile = container.resolve(newName);
    for (int count = 0; newFile != null && count < 999; ) {
      count++;
      newName = FileUtils.appendNumberAtTheEndOfFilename(filename, count);
      newFile = container.resolve(newName);
    }
    if (newFile == null) {
      return newName;
    }
    return null;
  }

  /**
   * Check if the file exist or not
   *
   * @param item
   * @return
   */
  public static boolean exists(VFSItem item) {
    if (item instanceof NamedContainerImpl) {
      item = ((NamedContainerImpl) item).getDelegate();
    }
    if (item instanceof MergeSource) {
      MergeSource source = (MergeSource) item;
      item = source.getRootWriteContainer();
      if (item == null) {
        // no write container, but the virtual container exist
        return true;
      }
    }
    if (item instanceof LocalImpl) {
      LocalImpl localFile = (LocalImpl) item;
      return localFile.getBasefile() != null && localFile.getBasefile().exists();
    }
    return false;
  }
}
/**
 * Initial date: 18.08.2015<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
@Service
public class HomeCalendarManager implements PersonalCalendarManager {

  private static final OLog log = Tracing.createLoggerFor(HomeCalendarManager.class);

  @Autowired private DB dbInstance;
  @Autowired private CalendarModule calendarModule;
  @Autowired private CalendarManager calendarManager;
  @Autowired private BusinessGroupService businessGroupService;
  @Autowired private ImportCalendarManager importCalendarManager;

  @Override
  public List<CalendarFileInfos> getListOfCalendarsFiles(Identity identity) {
    List<CalendarFileInfos> aggregatedFiles = new ArrayList<>();

    Map<CalendarKey, CalendarUserConfiguration> configMap =
        calendarManager.getCalendarUserConfigurationsMap(identity);

    // personal calendar
    CalendarKey personalCalendarKey =
        new CalendarKey(identity.getName(), CalendarManager.TYPE_USER);
    CalendarUserConfiguration personalCalendarConfig = configMap.get(personalCalendarKey);
    if (calendarModule.isEnablePersonalCalendar()
        && (personalCalendarConfig == null || personalCalendarConfig.isInAggregatedFeed())) {
      File iCalFile =
          calendarManager.getCalendarICalFile(CalendarManager.TYPE_USER, identity.getName());
      if (iCalFile != null) {
        aggregatedFiles.add(
            new CalendarFileInfos(identity.getName(), CalendarManager.TYPE_USER, iCalFile));
      }

      // reload every hour
      List<CalendarFileInfos> importedCalendars =
          importCalendarManager.getImportedCalendarInfosForIdentity(identity, true);
      aggregatedFiles.addAll(importedCalendars);
    }

    // group calendars
    if (calendarModule.isEnableGroupCalendar()) {
      SearchBusinessGroupParams groupParams = new SearchBusinessGroupParams(identity, true, true);
      groupParams.addTools(CollaborationTools.TOOL_CALENDAR);
      List<BusinessGroup> groups =
          businessGroupService.findBusinessGroups(groupParams, null, 0, -1);
      for (BusinessGroup group : groups) {
        String calendarId = group.getKey().toString();
        CalendarKey key = new CalendarKey(calendarId, CalendarManager.TYPE_GROUP);
        CalendarUserConfiguration calendarConfig = configMap.get(key);
        if (calendarConfig == null || calendarConfig.isInAggregatedFeed()) {
          File iCalFile =
              calendarManager.getCalendarICalFile(CalendarManager.TYPE_GROUP, calendarId);
          if (iCalFile != null) {
            aggregatedFiles.add(
                new CalendarFileInfos(calendarId, CalendarManager.TYPE_GROUP, iCalFile));
          }
        }
      }
    }

    if (calendarModule.isEnableCourseElementCalendar()
        || calendarModule.isEnableCourseToolCalendar()) {
      List<Object[]> resources = getCourses(identity);
      for (Object[] resource : resources) {
        RepositoryEntry courseEntry = (RepositoryEntry) resource[0];
        String calendarId = courseEntry.getOlatResource().getResourceableId().toString();
        CalendarKey key = new CalendarKey(calendarId, CalendarManager.TYPE_COURSE);
        CalendarUserConfiguration calendarConfig = configMap.get(key);
        if (calendarConfig == null || calendarConfig.isInAggregatedFeed()) {
          File iCalFile =
              calendarManager.getCalendarICalFile(CalendarManager.TYPE_COURSE, calendarId);
          if (iCalFile != null) {
            aggregatedFiles.add(
                new CalendarFileInfos(calendarId, CalendarManager.TYPE_COURSE, iCalFile));
          }
        }
      }
    }

    return aggregatedFiles;
  }

  @Override
  public List<KalendarRenderWrapper> getListOfCalendarWrappers(
      UserRequest ureq, WindowControl wControl) {
    if (!calendarModule.isEnabled()) {
      return new ArrayList<KalendarRenderWrapper>();
    }

    Identity identity = ureq.getIdentity();

    List<KalendarRenderWrapper> calendars = new ArrayList<KalendarRenderWrapper>();
    Map<CalendarKey, CalendarUserConfiguration> configMap =
        calendarManager.getCalendarUserConfigurationsMap(ureq.getIdentity());
    appendPersonalCalendar(identity, calendars, configMap);
    appendGroupCalendars(identity, calendars, configMap);
    appendCourseCalendars(ureq, wControl, calendars, configMap);

    // reload every hour
    List<KalendarRenderWrapper> importedCalendars =
        importCalendarManager.getImportedCalendarsForIdentity(identity, true);
    for (KalendarRenderWrapper importedCalendar : importedCalendars) {
      importedCalendar.setPrivateEventsVisible(true);
    }

    calendars.addAll(importedCalendars);
    return calendars;
  }

  private void appendPersonalCalendar(
      Identity identity,
      List<KalendarRenderWrapper> calendars,
      Map<CalendarKey, CalendarUserConfiguration> configMap) {
    // get the personal calendar
    if (calendarModule.isEnablePersonalCalendar()) {
      KalendarRenderWrapper calendarWrapper = calendarManager.getPersonalCalendar(identity);
      calendarWrapper.setAccess(KalendarRenderWrapper.ACCESS_READ_WRITE);
      calendarWrapper.setPrivateEventsVisible(true);
      CalendarUserConfiguration config = configMap.get(calendarWrapper.getCalendarKey());
      if (config != null) {
        calendarWrapper.setConfiguration(config);
      }
      calendars.add(calendarWrapper);
    }
  }

  private void appendGroupCalendars(
      Identity identity,
      List<KalendarRenderWrapper> calendars,
      Map<CalendarKey, CalendarUserConfiguration> configMap) {
    // get group calendars
    if (calendarModule.isEnableGroupCalendar()) {
      SearchBusinessGroupParams groupParams = new SearchBusinessGroupParams(identity, true, false);
      groupParams.addTools(CollaborationTools.TOOL_CALENDAR);
      List<BusinessGroup> ownerGroups =
          businessGroupService.findBusinessGroups(groupParams, null, 0, -1);
      addCalendars(ownerGroups, true, false, calendars, configMap);

      SearchBusinessGroupParams groupParams2 = new SearchBusinessGroupParams(identity, false, true);
      groupParams2.addTools(CollaborationTools.TOOL_CALENDAR);
      List<BusinessGroup> attendedGroups =
          businessGroupService.findBusinessGroups(groupParams2, null, 0, -1);
      attendedGroups.removeAll(ownerGroups);
      addCalendars(attendedGroups, false, true, calendars, configMap);
    }
  }

  private void appendCourseCalendars(
      UserRequest ureq,
      WindowControl wControl,
      List<KalendarRenderWrapper> calendars,
      Map<CalendarKey, CalendarUserConfiguration> configMap) {
    if (calendarModule.isEnableCourseElementCalendar()
        || calendarModule.isEnableCourseToolCalendar()) {

      // add course calendars
      List<Object[]> resources = getCourses(ureq.getIdentity());
      Set<OLATResource> editoredResources = getEditorGrants(ureq.getIdentity());

      Set<Long> duplicates = new HashSet<>();

      for (Object[] resource : resources) {
        RepositoryEntry courseEntry = (RepositoryEntry) resource[0];
        if (duplicates.contains(courseEntry.getKey())) {
          continue;
        }
        duplicates.add(courseEntry.getKey());

        String role = (String) resource[1];
        Long courseResourceableID = courseEntry.getOlatResource().getResourceableId();
        try {
          ICourse course = CourseFactory.loadCourse(courseEntry);
          if (isCourseCalendarEnabled(course)) {
            // calendar course aren't enabled per default but course node of type calendar are
            // always possible
            // REVIEW if (!course.getCourseEnvironment().getCourseConfig().isCalendarEnabled())
            // continue;
            // add course calendar
            KalendarRenderWrapper courseCalendarWrapper = calendarManager.getCourseCalendar(course);
            boolean isPrivileged =
                GroupRoles.owner.name().equals(role)
                    || editoredResources.contains(courseEntry.getOlatResource());
            if (isPrivileged) {
              courseCalendarWrapper.setAccess(KalendarRenderWrapper.ACCESS_READ_WRITE);
            } else {
              courseCalendarWrapper.setAccess(KalendarRenderWrapper.ACCESS_READ_ONLY);
            }

            if (role != null
                && (GroupRoles.owner.name().equals(role)
                    || GroupRoles.coach.name().equals(role)
                    || GroupRoles.participant.name().equals(role))) {
              courseCalendarWrapper.setPrivateEventsVisible(true);
            }

            CalendarUserConfiguration config =
                configMap.get(courseCalendarWrapper.getCalendarKey());
            if (config != null) {
              courseCalendarWrapper.setConfiguration(config);
            }
            courseCalendarWrapper.setLinkProvider(
                new CourseLinkProviderController(
                    course, Collections.singletonList(course), ureq, wControl));
            calendars.add(courseCalendarWrapper);
          }
        } catch (CorruptedCourseException e) {
          OLATResource olatResource = courseEntry.getOlatResource();
          log.error(
              "Corrupted course: "
                  + olatResource.getResourceableTypeName()
                  + " :: "
                  + courseResourceableID,
              null);
        }
      }
    }
  }

  private boolean isCourseCalendarEnabled(ICourse course) {
    if (course.getCourseConfig().isCalendarEnabled()) {
      return true;
    }

    CourseNode rootNode = course.getRunStructure().getRootNode();
    CalCourseNodeVisitor v = new CalCourseNodeVisitor();
    new TreeVisitor(v, rootNode, true).visitAll();
    return v.isFound();
  }

  /**
   * @param identity
   * @return List of array, first the repository entry, second the role
   */
  private List<Object[]> getCourses(IdentityRef identity) {
    StringBuilder sb = new StringBuilder();
    sb.append("select v, membership.role from repositoryentry  v ")
        .append(" inner join fetch v.olatResource as resource ")
        .append(" inner join v.groups as retogroup")
        .append(" inner join retogroup.group as baseGroup")
        .append(" inner join baseGroup.members as membership")
        .append(
            " where v.olatResource.resName='CourseModule' and membership.identity.key=:identityKey and")
        .append(" (")
        .append("   (v.access=")
        .append(RepositoryEntry.ACC_OWNERS)
        .append(" and v.membersOnly=true and membership.role in ('")
        .append(GroupRoles.owner.name())
        .append("','")
        .append(GroupRoles.coach.name())
        .append("','")
        .append(GroupRoles.participant.name())
        .append("'))")
        .append("   or")
        .append("   (v.access>=")
        .append(RepositoryEntry.ACC_OWNERS)
        .append(" and membership.role='")
        .append(GroupRoles.owner.name())
        .append("')")
        .append("   or")
        .append("   (v.access>=")
        .append(RepositoryEntry.ACC_USERS)
        .append(" and membership.role in ('")
        .append(GroupRoles.coach.name())
        .append("','")
        .append(GroupRoles.participant.name())
        .append("'))")
        .append(" )");

    return dbInstance
        .getCurrentEntityManager()
        .createQuery(sb.toString(), Object[].class)
        .setParameter("identityKey", identity.getKey())
        .getResultList();
  }

  private Set<OLATResource> getEditorGrants(IdentityRef identity) {
    StringBuilder sb = new StringBuilder();
    sb.append("select grant.resource from bgrant as grant")
        .append(" inner join grant.group as baseGroup")
        .append(" inner join baseGroup.members as membership")
        .append(" where membership.identity.key=:identityKey and grant.permission='")
        .append(CourseRights.RIGHT_COURSEEDITOR)
        .append("' and membership.role=grant.role");
    List<OLATResource> resources =
        dbInstance
            .getCurrentEntityManager()
            .createQuery(sb.toString(), OLATResource.class)
            .setParameter("identityKey", identity.getKey())
            .getResultList();
    return new HashSet<>(resources);
  }

  /**
   * Append the calendars of a list of groups. The groups must have their calendar tool enabled,
   * this routine doesn't check it.
   *
   * @param ureq
   * @param groups
   * @param isOwner
   * @param calendars
   */
  private void addCalendars(
      List<BusinessGroup> groups,
      boolean isOwner,
      boolean isParticipant,
      List<KalendarRenderWrapper> calendars,
      Map<CalendarKey, CalendarUserConfiguration> configMap) {

    Map<Long, Long> groupKeyToAccess =
        CoreSpringFactory.getImpl(CollaborationManager.class).lookupCalendarAccess(groups);
    for (BusinessGroup bGroup : groups) {
      KalendarRenderWrapper groupCalendarWrapper = calendarManager.getGroupCalendar(bGroup);
      groupCalendarWrapper.setPrivateEventsVisible(true);
      // set calendar access
      int iCalAccess = CollaborationTools.CALENDAR_ACCESS_OWNERS;
      Long lCalAccess = groupKeyToAccess.get(bGroup.getKey());
      if (lCalAccess != null) {
        iCalAccess = lCalAccess.intValue();
      }
      if (iCalAccess == CollaborationTools.CALENDAR_ACCESS_OWNERS && !isOwner) {
        groupCalendarWrapper.setAccess(KalendarRenderWrapper.ACCESS_READ_ONLY);
      } else {
        groupCalendarWrapper.setAccess(KalendarRenderWrapper.ACCESS_READ_WRITE);
      }
      CalendarUserConfiguration config = configMap.get(groupCalendarWrapper.getCalendarKey());
      if (config != null) {
        groupCalendarWrapper.setConfiguration(config);
      }
      if (isOwner || isParticipant) {
        groupCalendarWrapper.setPrivateEventsVisible(true);
      }
      calendars.add(groupCalendarWrapper);
    }
  }

  private static class CalCourseNodeVisitor implements Visitor {
    private boolean found = false;

    public boolean isFound() {
      return found;
    }

    @Override
    public void visit(INode node) {
      if (node instanceof CalCourseNode) {
        found = true;
      }
    }
  }
}
/**
 * Description:<br>
 * The rating controller offers a view for users to rate on something using a star-like view.
 *
 * <h3>Events fired by this conmponent:</h3>
 *
 * RatingEvent that contains the new rating
 *
 * <p>Initial Date: 31.10.2008 <br>
 *
 * @author gnaegi
 */
public class RatingComponent extends Component {
  private static final OLog log = Tracing.createLoggerFor(RatingComponent.class);
  private static final ComponentRenderer RENDERER = new RatingRenderer();
  private List<String> ratingLabels;
  private boolean translateRatingLabels;
  private String title;
  private boolean translateTitle;
  private String explanation;
  private boolean translateExplanation;
  private boolean showRatingAsText;
  private boolean allowUserInput;
  private String cssClass;
  private float currentRating;

  /**
   * Create a rating component with no title and a default explanation and hover texts. Use the
   * setter methods to change the values. Use NULL values to disable texts (title, explanation,
   * labels)
   *
   * @param name
   * @param currentRating the current rating
   * @param maxRating maximum number that can be rated
   * @param allowUserInput
   */
  public RatingComponent(String name, float currentRating, int maxRating, boolean allowUserInput) {
    super(name);
    if (currentRating > maxRating)
      throw new AssertException(
          "Current rating set to higher value::"
              + currentRating
              + " than the maxRating::"
              + maxRating);
    this.allowUserInput = allowUserInput;
    this.currentRating = currentRating;
    // use default values for the other stuff
    this.ratingLabels = new ArrayList<String>(maxRating);
    for (int i = 0; i < maxRating; i++) {
      // style: rating.5.3 => 3 out of 5
      this.ratingLabels.add("rating." + maxRating + "." + (i + 1));
    }
    this.translateRatingLabels = true;
    this.title = null;
    this.translateTitle = true;
    if (allowUserInput) this.explanation = "rating.explanation";
    else this.explanation = null;
    this.translateExplanation = true;
    this.showRatingAsText = false;
  }

  /**
   * @see org.olat.core.gui.components.Component#doDispatchRequest(org.olat.core.gui.UserRequest)
   */
  @Override
  protected void doDispatchRequest(UserRequest ureq) {
    setDirty(true);
    String cmd = ureq.getParameter(VelocityContainer.COMMAND_ID);
    if (log.isDebug()) {
      log.debug("***RATING_CLICKED*** dispatchID::" + ureq.getComponentID() + " rating::" + cmd);
    }
    try {
      float rating = Float.parseFloat(cmd);
      // update GUI
      this.setCurrentRating(rating);
      // notify listeners
      Event event = new RatingEvent(rating);
      fireEvent(ureq, event);
    } catch (NumberFormatException e) {
      log.error("Error while parsing rating value::" + cmd);
    }
  }

  /** @see org.olat.core.gui.components.Component#getHTMLRendererSingleton() */
  @Override
  public ComponentRenderer getHTMLRendererSingleton() {
    return RENDERER;
  }

  //
  // Various getter and setter methods
  //

  // only package scope, used by renderer
  List<String> getRatingLabel() {
    return ratingLabels;
  }

  public String getRatingLabel(int position) {
    if (position >= ratingLabels.size()) {
      throw new AssertException(
          "Can not get rating at position::"
              + position
              + " in rating array of size::"
              + ratingLabels.size()
              + " in component::"
              + getComponentName());
    }
    return ratingLabels.get(position);
  }

  public void setLevelLabel(int position, String ratingLabel) {
    if (position >= ratingLabels.size()) {
      throw new AssertException(
          "Can not set rating at position::"
              + position
              + " in rating array of size::"
              + ratingLabels.size()
              + " in component::"
              + getComponentName());
    }
    this.ratingLabels.set(position, ratingLabel);
    this.setDirty(true);
  }

  public boolean isTranslateRatingLabels() {
    return translateRatingLabels;
  }

  public void setTranslateRatingLabels(boolean translateRatingLabels) {
    this.translateRatingLabels = translateRatingLabels;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
    this.setDirty(true);
  }

  public boolean isTranslateTitle() {
    return translateTitle;
  }

  public void setTranslateTitle(boolean translateTitle) {
    this.translateTitle = translateTitle;
  }

  public String getExplanation() {
    return explanation;
  }

  public void setExplanation(String explanation) {
    this.explanation = explanation;
    this.setDirty(true);
  }

  public boolean isTranslateExplanation() {
    return translateExplanation;
  }

  public void setTranslateExplanation(boolean translateExplanation) {
    this.translateExplanation = translateExplanation;
  }

  public boolean isShowRatingAsText() {
    return showRatingAsText;
  }

  public void setShowRatingAsText(boolean showRatingAsText) {
    this.showRatingAsText = showRatingAsText;
  }

  public boolean isAllowUserInput() {
    return allowUserInput;
  }

  public void setAllowUserInput(boolean allowUserInput) {
    this.allowUserInput = allowUserInput;
    this.setDirty(true);
  }

  public int getRatingSteps() {
    return ratingLabels.size();
  }

  public String getCssClass() {
    return cssClass;
  }

  public void setCssClass(String cssClass) {
    this.cssClass = cssClass;
    this.setDirty(true);
  }

  public float getCurrentRating() {
    return currentRating;
  }

  public void setCurrentRating(float currentRating) {
    this.currentRating = currentRating;
    this.setDirty(true);
  }
}
Example #10
0
/**
 *
 *
 * <h3>Description:</h3>
 *
 * The YearPropertyHandler offers the functionality of a Year date. It can be used to store
 * something like a graduation date. The property is rendered as a dropdown menu. (from- , to- year
 * values can be configured through <code>YearPropertyHandlerController</code>)
 *
 * <p>Initial Date: 15.12.2011 <br>
 *
 * @author strentini, [email protected], www.frentix.com
 */
public class YearPropertyHandler extends AbstractUserPropertyHandler {

  OLog logger = Tracing.createLoggerFor(getClass());

  public static final String PROP_FROM = "yph.from";
  public static final String PROP_TO = "yph.to";

  private static final String NO_SEL_KEY = "select";
  private String[] selectionKeys = getDefaultYears();
  private UsrPropHandlerCfgFactory cfgFactory;

  @Override
  public FormItem addFormItem(
      Locale locale,
      User user,
      String usageIdentifyer,
      boolean isAdministrativeUser,
      FormItemContainer formItemContainer) {

    /* let's load the years */
    loadSelectionKeysFromConfig();

    // add the no-selection entry to the dropdown
    String[] allKeys = new String[selectionKeys.length + 1];
    System.arraycopy(selectionKeys, 0, allKeys, 1, selectionKeys.length);
    allKeys[0] = NO_SEL_KEY;

    SingleSelection sse =
        FormUIFactory.getInstance()
            .addDropdownSingleselect(
                getName(), i18nFormElementLabelKey(), formItemContainer, allKeys, allKeys, null);
    String internalValue = getInternalValue(user);
    if (isValidValue(user, internalValue, null, null) && internalValue != null)
      sse.select(internalValue, true);

    // enable/disable according to settings
    UserManager um = UserManager.getInstance();
    if (um.isUserViewReadOnly(usageIdentifyer, this) && !isAdministrativeUser) {
      sse.setEnabled(false);
    }
    if (um.isMandatoryUserProperty(usageIdentifyer, this)) {
      sse.setMandatory(true);
    }

    return sse;
  }

  private void loadSelectionKeysFromConfig() {
    Map<String, String> handlerConfig = cfgFactory.loadConfigForHandler(this);

    // now "calculate" available year-values for dropdown, according to
    // handler config
    if (handlerConfig.containsKey(PROP_FROM) && handlerConfig.containsKey(PROP_TO)) {
      // we have a valid config
      int nowYear = Calendar.getInstance().get(Calendar.YEAR);

      String from = handlerConfig.get(PROP_FROM);
      String to = handlerConfig.get(PROP_TO);
      int i_from = 1900;
      int i_to = 1900;

      if (from.startsWith("+")) i_from = nowYear + Integer.parseInt(from.substring(1));
      else if (from.startsWith("-")) i_from = nowYear - Integer.parseInt(from.substring(1));
      else i_from = Integer.parseInt(from);

      if (to.startsWith("+")) i_to = nowYear + Integer.parseInt(to.substring(1));
      else if (to.startsWith("-")) i_to = nowYear - Integer.parseInt(to.substring(1));
      else i_to = Integer.parseInt(to);

      if (i_to < i_from) {
        logger.warn("wrong config in YearPropertyHandler : to is smaller than from...");
        // leave selectionKeys to default
        selectionKeys = getDefaultYears();
      } else {
        // now fill the array
        int span = i_to - i_from;
        if (span > 1000) span = 1000; // just prevent toooooo long dropdown-list ^
        selectionKeys = new String[span + 1];
        for (int j = 0; j <= span; j++) selectionKeys[j] = String.valueOf(i_from + j);
      }
    }
  }

  @Override
  public void updateUserFromFormItem(User user, FormItem formItem) {
    String internalValue = getStringValue(formItem);
    setInternalValue(user, internalValue);
  }

  @Override
  public boolean isValid(User user, FormItem formItem, Map<String, String> formContext) {
    if (formItem.isMandatory()) {
      SingleSelection ssel = (SingleSelection) formItem;
      if (ssel.getSelectedKey().equals(NO_SEL_KEY)) {
        ssel.setErrorKey("form.legende.mandatory", null);
        return false;
      }
    }
    return true;
  }

  @Override
  public boolean isValidValue(
      User user, String value, ValidationError validationError, Locale locale) {
    if (value != null) {
      for (int i = 0; i < selectionKeys.length; i++) {
        String key = selectionKeys[i];
        if (key.equals(value)) {
          return true;
        }
      }
      return false;
    }
    return true;
  }

  @Override
  public String getStringValue(FormItem formItem) {
    return ((SingleSelection) formItem).getSelectedKey();
  }

  @Override
  public String getStringValue(String displayValue, Locale locale) {
    return displayValue;
  }

  /** @return */
  public UsrPropHandlerCfgFactory getHandlerConfigFactory() {
    return cfgFactory;
  }

  /**
   * [spring] setter
   *
   * @param factory
   */
  public void setHandlerConfigFactory(UsrPropHandlerCfgFactory factory) {
    cfgFactory = factory;
  }

  /**
   * returns an array of 10 year-strings (5 years back from current year, to 5 years into the
   * future)<br>
   * e.g., if called in 2000, this method returns an array : [1995,1996,1997,.....,2004,2005]
   *
   * @return
   */
  private static String[] getDefaultYears() {
    int nowYear = Calendar.getInstance().get(Calendar.YEAR);
    int from = nowYear - 5;
    int to = nowYear + 5;
    int span = to - from;
    String[] years = new String[span];

    for (int j = 0; j < span; j++) years[j] = String.valueOf(from + j);

    return years;
  }
}
Example #11
0
/**
 * The default resource-serving servlet for most web applications, used to serve static resources
 * such as HTML pages and images.
 *
 * <p>This servlet is intended to be mapped to <em>/</em> e.g.:
 *
 * <pre>
 *   &lt;servlet-mapping&gt;
 *       &lt;servlet-name&gt;default&lt;/servlet-name&gt;
 *       &lt;url-pattern&gt;/&lt;/url-pattern&gt;
 *   &lt;/servlet-mapping&gt;
 * </pre>
 *
 * <p>It can be mapped to sub-paths, however in all cases resources are served from the web
 * appplication resource root using the full path from the root of the web application context. <br>
 * e.g. given a web application structure:
 *
 * <pre>
 * /context
 *   /images
 *     tomcat2.jpg
 *   /static
 *     /images
 *       tomcat.jpg
 * </pre>
 *
 * <p>... and a servlet mapping that maps only <code>/static/*</code> to the default servlet:
 *
 * <pre>
 *   &lt;servlet-mapping&gt;
 *       &lt;servlet-name&gt;default&lt;/servlet-name&gt;
 *       &lt;url-pattern&gt;/static/*&lt;/url-pattern&gt;
 *   &lt;/servlet-mapping&gt;
 * </pre>
 *
 * <p>Then a request to <code>/context/static/images/tomcat.jpg</code> will succeed while a request
 * to <code>/context/images/tomcat2.jpg</code> will fail.
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 * @version $Id$
 */
public abstract class DefaultDispatcher implements Serializable {

  private static final long serialVersionUID = 1L;
  private static final OLog log = Tracing.createLoggerFor(DefaultDispatcher.class);

  // ----------------------------------------------------- Instance Variables

  /** The input buffer size to use when serving resources. */
  private int input = 2048;

  /** Should be serve gzip versions of files. By default, it's set to false. */
  private boolean gzip = false;

  /** The output buffer size to use when serving resources. */
  private int output = 2048;

  /** Array containing the safe characters set. */
  private static final URLEncoder urlEncoder;

  /**
   * File encoding to be used when reading static files. If none is specified the platform default
   * is used.
   */
  protected String fileEncoding = null;

  /** Should the Accept-Ranges: bytes header be send with static resources? */
  private boolean useAcceptRanges = true;

  /** Full range marker. */
  private static final ArrayList<Range> FULL = new ArrayList<Range>();

  // ----------------------------------------------------- Static Initializer

  /** GMT timezone - all HTTP dates are on GMT */
  static {
    urlEncoder = new URLEncoder();
    urlEncoder.addSafeCharacter('-');
    urlEncoder.addSafeCharacter('_');
    urlEncoder.addSafeCharacter('.');
    urlEncoder.addSafeCharacter('*');
    urlEncoder.addSafeCharacter('/');
  }

  /** MIME multipart separation string */
  protected static final String mimeSeparation = "CATALINA_MIME_BOUNDARY";

  /** Size of file transfer buffer in bytes. */
  protected static final int BUFFER_SIZE = 4096;

  // --------------------------------------------------------- Public Methods

  protected abstract WebResourceRoot getResources(HttpServletRequest req);

  // ------------------------------------------------------ Protected Methods

  /**
   * Return the relative path associated with this servlet.
   *
   * @param request The servlet request we are processing
   */
  protected String getRelativePath(HttpServletRequest request) {
    // IMPORTANT: DefaultServlet can be mapped to '/' or '/path/*' but always
    // serves resources from the web app root with context rooted paths.
    // i.e. it cannot be used to mount the web app root under a sub-path
    // This method must construct a complete context rooted path, although
    // subclasses can change this behaviour.

    // Are we being processed by a RequestDispatcher.include()?
    if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
      String result = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
      if (result == null) {
        result = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
      } else {
        result = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH) + result;
      }
      if ((result == null) || (result.equals(""))) {
        result = "/";
      }
      return (result);
    }

    // No, extract the desired path directly from the request
    String result = request.getPathInfo();
    if (result == null) {
      result = request.getServletPath();
    } else {
      result = request.getServletPath() + result;
    }
    if ((result == null) || (result.equals(""))) {
      result = "/";
    }
    return (result);
  }

  /**
   * Determines the appropriate path to prepend resources with when generating directory listings.
   * Depending on the behaviour of {@link #getRelativePath(HttpServletRequest)} this will change.
   *
   * @param request the request to determine the path for
   * @return the prefix to apply to all resources in the listing.
   */
  protected String getPathPrefix(final HttpServletRequest request) {
    return request.getContextPath();
  }

  /**
   * Check if the conditions specified in the optional If headers are satisfied.
   *
   * @param request The servlet request we are processing
   * @param response The servlet response we are creating
   * @param resource The resource
   * @return boolean true if the resource meets all the specified conditions, and false if any of
   *     the conditions is not satisfied, in which case request processing is stopped
   */
  protected boolean checkIfHeaders(
      HttpServletRequest request, HttpServletResponse response, WebResource resource)
      throws IOException {

    return checkIfMatch(request, response, resource)
        && checkIfModifiedSince(request, response, resource)
        && checkIfNoneMatch(request, response, resource)
        && checkIfUnmodifiedSince(request, response, resource);
  }

  /**
   * URL rewriter.
   *
   * @param path Path which has to be rewritten
   */
  protected String rewriteUrl(String path) {
    return urlEncoder.encode(path);
  }

  /**
   * Serve the specified resource, optionally including the data content.
   *
   * @param request The servlet request we are processing
   * @param response The servlet response we are creating
   * @param content Should the content be included?
   * @param encoding The encoding to use if it is necessary to access the source as characters
   *     rather than as bytes
   * @exception IOException if an input/output error occurs
   * @exception ServletException if a servlet-specified error occurs
   */
  protected void serveResource(
      HttpServletRequest request, HttpServletResponse response, boolean content, String encoding)
      throws IOException, ServletException {

    boolean serveContent = content;
    boolean debug = log.isDebug();

    // Identify the requested resource path
    String path = getRelativePath(request);
    if (debug) {
      if (serveContent)
        log.debug(
            "DefaultServlet.serveResource:  Serving resource '" + path + "' headers and data");
      else log.debug("DefaultServlet.serveResource:  Serving resource '" + path + "' headers only");
    }

    WebResourceRoot resources = getResources(request);
    WebResource resource = resources.getResource(path);

    if (!resource.exists()) {
      // Check if we're included so we can return the appropriate
      // missing resource name in the error
      String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
      if (requestUri == null) {
        requestUri = request.getRequestURI();
      } else {
        // We're included
        // SRV.9.3 says we must throw a FNFE
        throw new FileNotFoundException("defaultServlet.missingResource");
      }

      response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri);
      return;
    }

    // If the resource is not a collection, and the resource path
    // ends with "/" or "\", return NOT FOUND
    if (resource.isFile() && path.endsWith("/") || path.endsWith("\\")) {
      // Check if we're included so we can return the appropriate
      // missing resource name in the error
      String requestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
      if (requestUri == null) {
        requestUri = request.getRequestURI();
      }
      response.sendError(HttpServletResponse.SC_NOT_FOUND, requestUri);
      return;
    }

    boolean isError = response.getStatus() >= HttpServletResponse.SC_BAD_REQUEST;

    boolean included = false;
    // Check if the conditions specified in the optional If headers are
    // satisfied.
    if (resource.isFile()) {
      // Checking If headers
      included = (request.getAttribute(RequestDispatcher.INCLUDE_CONTEXT_PATH) != null);
      if (!included && !isError && !checkIfHeaders(request, response, resource)) {
        return;
      }
    }

    // Find content type.
    String contentType = resource.getMimeType();
    if (contentType == null) {
      contentType = request.getServletContext().getMimeType(resource.getName());
      resource.setMimeType(contentType);
    }

    // These need to reflect the original resource, not the potentially
    // gzip'd version of the resource so get them now if they are going to
    // be needed later
    String eTag = null;
    String lastModifiedHttp = null;
    if (resource.isFile() && !isError) {
      eTag = resource.getETag();
      lastModifiedHttp = resource.getLastModifiedHttp();
    }

    // Serve a gzipped version of the file if present
    boolean usingGzippedVersion = false;
    if (gzip && !included && resource.isFile() && !path.endsWith(".gz")) {
      WebResource gzipResource = resources.getResource(path + ".gz");
      if (gzipResource.exists() && gzipResource.isFile()) {
        Collection<String> varyHeaders = response.getHeaders("Vary");
        boolean addRequired = true;
        for (String varyHeader : varyHeaders) {
          if ("*".equals(varyHeader) || "accept-encoding".equalsIgnoreCase(varyHeader)) {
            addRequired = false;
            break;
          }
        }
        if (addRequired) {
          response.addHeader("Vary", "accept-encoding");
        }
        if (checkIfGzip(request)) {
          response.addHeader("Content-Encoding", "gzip");
          resource = gzipResource;
          usingGzippedVersion = true;
        }
      }
    }

    ArrayList<Range> ranges = null;
    long contentLength = -1L;

    if (resource.isDirectory()) {
      contentType = "text/html;charset=UTF-8";
    } else {
      if (!isError) {
        if (useAcceptRanges) {
          // Accept ranges header
          response.setHeader("Accept-Ranges", "bytes");
        }

        // Parse range specifier
        ranges = parseRange(request, response, resource);

        // ETag header
        response.setHeader("ETag", eTag);

        // Last-Modified header
        response.setHeader("Last-Modified", lastModifiedHttp);
      }

      // Get content length
      contentLength = resource.getContentLength();
      // Special case for zero length files, which would cause a
      // (silent) ISE when setting the output buffer size
      if (contentLength == 0L) {
        serveContent = false;
      }
    }

    ServletOutputStream ostream = null;
    PrintWriter writer = null;

    if (serveContent) {
      // Trying to retrieve the servlet output stream
      try {
        ostream = response.getOutputStream();
      } catch (IllegalStateException e) {
        // If it fails, we try to get a Writer instead if we're
        // trying to serve a text file
        if (!usingGzippedVersion
            && ((contentType == null)
                || (contentType.startsWith("text"))
                || (contentType.endsWith("xml"))
                || (contentType.contains("/javascript")))) {
          writer = response.getWriter();
          // Cannot reliably serve partial content with a Writer
          ranges = FULL;
        } else {
          throw e;
        }
      }
    }

    // Check to see if a Filter, Valve of wrapper has written some content.
    // If it has, disable range requests and setting of a content length
    // since neither can be done reliably.
    ServletResponse r = response;
    long contentWritten = 0;
    while (r instanceof ServletResponseWrapper) {
      r = ((ServletResponseWrapper) r).getResponse();
    }

    if (contentWritten > 0) {
      ranges = FULL;
    }

    if (resource.isDirectory()
        || isError
        || ((ranges == null || ranges.isEmpty()) && request.getHeader("Range") == null)
        || ranges == FULL) {

      // Set the appropriate output headers
      if (contentType != null) {
        if (debug) log.debug("DefaultServlet.serveFile:  contentType='" + contentType + "'");
        response.setContentType(contentType);
      }
      if (resource.isFile() && contentLength >= 0 && (!serveContent || ostream != null)) {
        if (debug) log.debug("DefaultServlet.serveFile:  contentLength=" + contentLength);
        // Don't set a content length if something else has already
        // written to the response.
        if (contentWritten == 0) {
          response.setContentLengthLong(contentLength);
        }
      }

      InputStream renderResult = null;
      if (resource.isDirectory()) {
        if (serveContent) {
          // Serve the directory browser
          renderResult = null; // TODO tomcat render(getPathPrefix(request), resource);
        }
      }

      // Copy the input stream to our output stream (if requested)
      if (serveContent) {
        resource.increaseDownloadCount();

        try {
          response.setBufferSize(output);
        } catch (IllegalStateException e) {
          // Silent catch
        }

        if (ostream == null) {
          // Output via a writer so can't use sendfile or write
          // content directly.
          if (resource.isDirectory()) {
            renderResult = null; // render(getPathPrefix(request), resource);
          } else {
            renderResult = resource.getInputStream();
          }
          copy(resource, renderResult, writer, encoding);
        } else {
          // Output is via an InputStream
          if (resource.isDirectory()) {
            renderResult = null; // render(getPathPrefix(request), resource);
          } else {
            renderResult = resource.getInputStream();
          }
          // If a stream was configured, it needs to be copied to
          // the output (this method closes the stream)
          if (renderResult != null) {
            copy(renderResult, ostream);
          }
        }
      }

    } else {
      // download counter
      resource.increaseDownloadCount();

      if ((ranges == null) || (ranges.isEmpty())) return;

      // Partial content response.

      response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

      if (ranges.size() == 1) {

        Range range = ranges.get(0);
        response.addHeader(
            "Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
        long length = range.end - range.start + 1;
        response.setContentLengthLong(length);

        if (contentType != null) {
          if (debug) log.debug("DefaultServlet.serveFile:  contentType='" + contentType + "'");
          response.setContentType(contentType);
        }

        if (serveContent) {
          try {
            response.setBufferSize(output);
          } catch (IllegalStateException e) {
            // Silent catch
          }
          if (ostream != null) {
            copy(resource, ostream, range);
          } else {
            // we should not get here
            throw new IllegalStateException();
          }
        }
      } else {
        response.setContentType("multipart/byteranges; boundary=" + mimeSeparation);
        if (serveContent) {
          try {
            response.setBufferSize(output);
          } catch (IllegalStateException e) {
            // Silent catch
          }
          if (ostream != null) {
            copy(resource, ostream, ranges.iterator(), contentType);
          } else {
            // we should not get here
            throw new IllegalStateException();
          }
        }
      }
    }
  }

  /**
   * Parse the content-range header.
   *
   * @param request The servlet request we a)re processing
   * @param response The servlet response we are creating
   * @return Range
   */
  protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response)
      throws IOException {

    // Retrieving the content-range header (if any is specified
    String rangeHeader = request.getHeader("Content-Range");

    if (rangeHeader == null) return null;

    // bytes is the only range unit supported
    if (!rangeHeader.startsWith("bytes")) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return null;
    }

    rangeHeader = rangeHeader.substring(6).trim();

    int dashPos = rangeHeader.indexOf('-');
    int slashPos = rangeHeader.indexOf('/');

    if (dashPos == -1) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return null;
    }

    if (slashPos == -1) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return null;
    }

    Range range = new Range();

    try {
      range.start = Long.parseLong(rangeHeader.substring(0, dashPos));
      range.end = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));
      range.length = Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length()));
    } catch (NumberFormatException e) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return null;
    }

    if (!range.validate()) {
      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
      return null;
    }

    return range;
  }

  /**
   * Parse the range header.
   *
   * @param request The servlet request we are processing
   * @param response The servlet response we are creating
   * @param resource The resource
   * @return Vector of ranges
   */
  protected ArrayList<Range> parseRange(
      HttpServletRequest request, HttpServletResponse response, WebResource resource)
      throws IOException {

    // Checking If-Range
    String headerValue = request.getHeader("If-Range");

    if (headerValue != null) {

      long headerValueTime = (-1L);
      try {
        headerValueTime = request.getDateHeader("If-Range");
      } catch (IllegalArgumentException e) {
        // Ignore
      }

      String eTag = resource.getETag();
      long lastModified = resource.getLastModified();

      if (headerValueTime == (-1L)) {

        // If the ETag the client gave does not match the entity
        // etag, then the entire entity is returned.
        if (!eTag.equals(headerValue.trim())) return FULL;

      } else {

        // If the timestamp of the entity the client got is older than
        // the last modification date of the entity, the entire entity
        // is returned.
        if (lastModified > (headerValueTime + 1000)) return FULL;
      }
    }

    long fileLength = resource.getContentLength();

    if (fileLength == 0) return null;

    // Retrieving the range header (if any is specified
    String rangeHeader = request.getHeader("Range");

    if (rangeHeader == null) return null;
    // bytes is the only range unit supported (and I don't see the point
    // of adding new ones).
    if (!rangeHeader.startsWith("bytes")) {
      response.addHeader("Content-Range", "bytes */" + fileLength);
      response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
      return null;
    }

    rangeHeader = rangeHeader.substring(6);

    // Vector which will contain all the ranges which are successfully
    // parsed.
    ArrayList<Range> result = new ArrayList<>();
    StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");

    // Parsing the range list
    while (commaTokenizer.hasMoreTokens()) {
      String rangeDefinition = commaTokenizer.nextToken().trim();

      Range currentRange = new Range();
      currentRange.length = fileLength;

      int dashPos = rangeDefinition.indexOf('-');

      if (dashPos == -1) {
        response.addHeader("Content-Range", "bytes */" + fileLength);
        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
        return null;
      }

      if (dashPos == 0) {

        try {
          long offset = Long.parseLong(rangeDefinition);
          currentRange.start = fileLength + offset;
          currentRange.end = fileLength - 1;
        } catch (NumberFormatException e) {
          response.addHeader("Content-Range", "bytes */" + fileLength);
          response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
          return null;
        }

      } else {

        try {
          currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
          if (dashPos < rangeDefinition.length() - 1)
            currentRange.end =
                Long.parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length()));
          else currentRange.end = fileLength - 1;
        } catch (NumberFormatException e) {
          response.addHeader("Content-Range", "bytes */" + fileLength);
          response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
          return null;
        }
      }

      if (!currentRange.validate()) {
        response.addHeader("Content-Range", "bytes */" + fileLength);
        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
        return null;
      }

      result.add(currentRange);
    }

    return result;
  }

  // -------------------------------------------------------- protected Methods

  /**
   * Check if the if-match condition is satisfied.
   *
   * @param request The servlet request we are processing
   * @param response The servlet response we are creating
   * @param resource The resource
   * @return boolean true if the resource meets the specified condition, and false if the condition
   *     is not satisfied, in which case request processing is stopped
   */
  protected boolean checkIfMatch(
      HttpServletRequest request, HttpServletResponse response, WebResource resource)
      throws IOException {

    String eTag = resource.getETag();
    String headerValue = request.getHeader("If-Match");
    if (headerValue != null) {
      if (headerValue.indexOf('*') == -1) {

        StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
        boolean conditionSatisfied = false;

        while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
          String currentToken = commaTokenizer.nextToken();
          if (currentToken.trim().equals(eTag)) conditionSatisfied = true;
        }

        // If none of the given ETags match, 412 Precodition failed is
        // sent back
        if (!conditionSatisfied) {
          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Check if the if-modified-since condition is satisfied.
   *
   * @param request The servlet request we are processing
   * @param response The servlet response we are creating
   * @param resource The resource
   * @return boolean true if the resource meets the specified condition, and false if the condition
   *     is not satisfied, in which case request processing is stopped
   */
  protected boolean checkIfModifiedSince(
      HttpServletRequest request, HttpServletResponse response, WebResource resource) {
    try {
      long headerValue = request.getDateHeader("If-Modified-Since");
      long lastModified = resource.getLastModified();
      if (headerValue != -1) {

        // If an If-None-Match header has been specified, if modified since
        // is ignored.
        if ((request.getHeader("If-None-Match") == null) && (lastModified < headerValue + 1000)) {
          // The entity has not been modified since the date
          // specified by the client. This is not an error case.
          response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
          response.setHeader("ETag", resource.getETag());

          return false;
        }
      }
    } catch (IllegalArgumentException illegalArgument) {
      return true;
    }
    return true;
  }

  /**
   * Check if the if-none-match condition is satisfied.
   *
   * @param request The servlet request we are processing
   * @param response The servlet response we are creating
   * @param resource The resource
   * @return boolean true if the resource meets the specified condition, and false if the condition
   *     is not satisfied, in which case request processing is stopped
   */
  protected boolean checkIfNoneMatch(
      HttpServletRequest request, HttpServletResponse response, WebResource resource)
      throws IOException {

    String eTag = resource.getETag();
    String headerValue = request.getHeader("If-None-Match");
    if (headerValue != null) {

      boolean conditionSatisfied = false;

      if (!headerValue.equals("*")) {

        StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");

        while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
          String currentToken = commaTokenizer.nextToken();
          if (currentToken.trim().equals(eTag)) conditionSatisfied = true;
        }

      } else {
        conditionSatisfied = true;
      }

      if (conditionSatisfied) {

        // For GET and HEAD, we should respond with
        // 304 Not Modified.
        // For every other method, 412 Precondition Failed is sent
        // back.
        if (("GET".equals(request.getMethod())) || ("HEAD".equals(request.getMethod()))) {
          response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
          response.setHeader("ETag", eTag);

          return false;
        }
        response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
        return false;
      }
    }
    return true;
  }

  /**
   * Check if the user agent supports gzip encoding.
   *
   * @param request The servlet request we are processing
   * @return boolean true if the user agent supports gzip encoding, and false if the user agent does
   *     not support gzip encoding
   */
  protected boolean checkIfGzip(HttpServletRequest request) {
    Enumeration<String> headers = request.getHeaders("Accept-Encoding");
    while (headers.hasMoreElements()) {
      String header = headers.nextElement();
      if (header.indexOf("gzip") != -1) {
        return true;
      }
    }
    return false;
  }

  /**
   * Check if the if-unmodified-since condition is satisfied.
   *
   * @param request The servlet request we are processing
   * @param response The servlet response we are creating
   * @param resource The resource
   * @return boolean true if the resource meets the specified condition, and false if the condition
   *     is not satisfied, in which case request processing is stopped
   */
  protected boolean checkIfUnmodifiedSince(
      HttpServletRequest request, HttpServletResponse response, WebResource resource)
      throws IOException {
    try {
      long lastModified = resource.getLastModified();
      long headerValue = request.getDateHeader("If-Unmodified-Since");
      if (headerValue != -1) {
        if (lastModified >= (headerValue + 1000)) {
          // The entity has not been modified since the date
          // specified by the client. This is not an error case.
          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
          return false;
        }
      }
    } catch (IllegalArgumentException illegalArgument) {
      return true;
    }
    return true;
  }

  /**
   * Copy the contents of the specified input stream to the specified output stream, and ensure that
   * both streams are closed before returning (even in the face of an exception).
   *
   * @param is The input stream to read the source resource from
   * @param ostream The output stream to write to
   * @exception IOException if an input/output error occurs
   */
  private void copy(InputStream is, ServletOutputStream ostream) throws IOException {

    IOException exception = null;
    InputStream istream = new BufferedInputStream(is, input);

    // Copy the input stream to the output stream
    exception = copyRange(istream, ostream);

    // Clean up the input stream
    istream.close();

    // Rethrow any exception that has occurred
    if (exception != null) throw exception;
  }

  /**
   * Copy the contents of the specified input stream to the specified output stream, and ensure that
   * both streams are closed before returning (even in the face of an exception).
   *
   * @param resource The source resource
   * @param is The input stream to read the source resource from
   * @param writer The writer to write to
   * @param encoding The encoding to use when reading the source input stream
   * @exception IOException if an input/output error occurs
   */
  protected void copy(WebResource resource, InputStream is, PrintWriter writer, String encoding)
      throws IOException {

    IOException exception = null;

    InputStream resourceInputStream = null;
    if (resource.isFile()) {
      resourceInputStream = resource.getInputStream();
    } else {
      resourceInputStream = is;
    }

    Reader reader;
    if (encoding == null) {
      reader = new InputStreamReader(resourceInputStream);
    } else {
      reader = new InputStreamReader(resourceInputStream, encoding);
    }

    // Copy the input stream to the output stream
    exception = copyRange(reader, writer);

    // Clean up the reader
    reader.close();

    // Rethrow any exception that has occurred
    if (exception != null) throw exception;
  }

  /**
   * Copy the contents of the specified input stream to the specified output stream, and ensure that
   * both streams are closed before returning (even in the face of an exception).
   *
   * @param resource The source resource
   * @param ostream The output stream to write to
   * @param range Range the client wanted to retrieve
   * @exception IOException if an input/output error occurs
   */
  protected void copy(WebResource resource, ServletOutputStream ostream, Range range)
      throws IOException {

    IOException exception = null;

    InputStream resourceInputStream = resource.getInputStream();
    InputStream istream = new BufferedInputStream(resourceInputStream, input);
    exception = copyRange(istream, ostream, range.start, range.end);

    // Clean up the input stream
    istream.close();

    // Rethrow any exception that has occurred
    if (exception != null) throw exception;
  }

  /**
   * Copy the contents of the specified input stream to the specified output stream, and ensure that
   * both streams are closed before returning (even in the face of an exception).
   *
   * @param resource The source resource
   * @param ostream The output stream to write to
   * @param ranges Enumeration of the ranges the client wanted to retrieve
   * @param contentType Content type of the resource
   * @exception IOException if an input/output error occurs
   */
  protected void copy(
      WebResource resource, ServletOutputStream ostream, Iterator<Range> ranges, String contentType)
      throws IOException {

    IOException exception = null;

    while ((exception == null) && (ranges.hasNext())) {

      InputStream resourceInputStream = resource.getInputStream();
      try (InputStream istream = new BufferedInputStream(resourceInputStream, input)) {

        Range currentRange = ranges.next();

        // Writing MIME header.
        ostream.println();
        ostream.println("--" + mimeSeparation);
        if (contentType != null) ostream.println("Content-Type: " + contentType);
        ostream.println(
            "Content-Range: bytes "
                + currentRange.start
                + "-"
                + currentRange.end
                + "/"
                + currentRange.length);
        ostream.println();

        // Printing content
        exception = copyRange(istream, ostream, currentRange.start, currentRange.end);
      }
    }

    ostream.println();
    ostream.print("--" + mimeSeparation + "--");

    // Rethrow any exception that has occurred
    if (exception != null) throw exception;
  }

  /**
   * Copy the contents of the specified input stream to the specified output stream, and ensure that
   * both streams are closed before returning (even in the face of an exception).
   *
   * @param istream The input stream to read from
   * @param ostream The output stream to write to
   * @return Exception which occurred during processing
   */
  protected IOException copyRange(InputStream istream, ServletOutputStream ostream) {

    // Copy the input stream to the output stream
    IOException exception = null;
    byte buffer[] = new byte[input];
    int len = buffer.length;
    while (true) {
      try {
        len = istream.read(buffer);
        if (len == -1) break;
        ostream.write(buffer, 0, len);
      } catch (IOException e) {
        exception = e;
        len = -1;
        break;
      }
    }
    return exception;
  }

  /**
   * Copy the contents of the specified input stream to the specified output stream, and ensure that
   * both streams are closed before returning (even in the face of an exception).
   *
   * @param reader The reader to read from
   * @param writer The writer to write to
   * @return Exception which occurred during processing
   */
  protected IOException copyRange(Reader reader, PrintWriter writer) {

    // Copy the input stream to the output stream
    IOException exception = null;
    char buffer[] = new char[input];
    int len = buffer.length;
    while (true) {
      try {
        len = reader.read(buffer);
        if (len == -1) break;
        writer.write(buffer, 0, len);
      } catch (IOException e) {
        exception = e;
        len = -1;
        break;
      }
    }
    return exception;
  }

  /**
   * Copy the contents of the specified input stream to the specified output stream, and ensure that
   * both streams are closed before returning (even in the face of an exception).
   *
   * @param istream The input stream to read from
   * @param ostream The output stream to write to
   * @param start Start of the range which will be copied
   * @param end End of the range which will be copied
   * @return Exception which occurred during processing
   */
  protected IOException copyRange(
      InputStream istream, ServletOutputStream ostream, long start, long end) {

    if (log.isDebug()) {
      log.debug("Serving bytes:" + start + "-" + end);
    }

    long skipped = 0;
    try {
      skipped = istream.skip(start);
    } catch (IOException e) {
      return e;
    }
    if (skipped < start) {
      return new IOException(
          "defaultservlet.skipfail" + Long.valueOf(skipped) + Long.valueOf(start));
    }

    IOException exception = null;
    long bytesToRead = end - start + 1;

    byte buffer[] = new byte[input];
    int len = buffer.length;
    while ((bytesToRead > 0) && (len >= buffer.length)) {
      try {
        len = istream.read(buffer);
        if (bytesToRead >= len) {
          ostream.write(buffer, 0, len);
          bytesToRead -= len;
        } else {
          ostream.write(buffer, 0, (int) bytesToRead);
          bytesToRead = 0;
        }
      } catch (IOException e) {
        exception = e;
        len = -1;
      }
      if (len < buffer.length) break;
    }

    return exception;
  }

  protected static class Range {

    public long start;
    public long end;
    public long length;

    /**
     * Validate range.
     *
     * @return true if the range is valid, otherwise false
     */
    public boolean validate() {
      if (end >= length) end = length - 1;
      return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
    }
  }
}
Example #12
0
public class PLockTest extends OlatTestCase {

  private static final OLog log = Tracing.createLoggerFor(PLockTest.class);

  private static final int MAX_COUNT = 5; // 5; //30;
  private static final int MAX_USERS_MORE = 20; // 20; //100;

  @Autowired private DB dbInstance;
  @Autowired private PessimisticLockManager pessimisticLockManager;

  @Test
  public void testReentrantLock() {
    long start = System.currentTimeMillis();
    String asset = "p1";
    // make sure the lock is created first
    PLock pc = pessimisticLockManager.findOrPersistPLock(asset);
    assertNotNull(pc);
    dbInstance.closeSession();

    // test double acquisition within same transaction
    PLock pc1 = pessimisticLockManager.findOrPersistPLock(asset);
    assertNotNull(pc1);
    PLock pc2 = pessimisticLockManager.findOrPersistPLock(asset);
    assertNotNull(pc2);
    dbInstance.closeSession();

    // and without explicit transaction boundary.
    PLock p1 = pessimisticLockManager.findOrPersistPLock(asset);
    assertNotNull(p1);
    PLock p2 = pessimisticLockManager.findOrPersistPLock(asset);
    assertNotNull(p2);
    long stop = System.currentTimeMillis();
    long diff = stop - start;
    assertTrue(
        "5 select's took longer than 10 seconds -> deadlock / lock timeout ? dur in ms was:" + diff,
        diff < 10000);
  }

  /** T1 T2 */
  @Test
  public void testReentrantLock2Threads() {
    final String asset = "p1-2";

    // make sure the lock is created first
    PLock pc = pessimisticLockManager.findOrPersistPLock(asset);
    assertNotNull(pc);
    dbInstance.closeSession();

    final List<Exception> exceptionHolder =
        Collections.synchronizedList(new ArrayList<Exception>(1));
    final CountDownLatch finishCount = new CountDownLatch(2);

    // thread 1
    new Thread(
            new Runnable() {
              public void run() {
                try {
                  PLock pc1 = pessimisticLockManager.findOrPersistPLock(asset);
                  assertNotNull(pc1);
                  log.info("Thread-1: got PLock pc1=" + pc1);
                  log.info("Thread-1: sleep 1sec");
                  sleep(1000);
                  PLock pc2 = pessimisticLockManager.findOrPersistPLock(asset);
                  assertNotNull(pc2);
                  log.info("Thread-1: got PLock pc2=" + pc2);
                  log.info("Thread-1: finished");
                } catch (Exception e) {
                  exceptionHolder.add(e);
                } finally {
                  finishCount.countDown();
                  try {
                    dbInstance.commitAndCloseSession();
                  } catch (Exception e) {
                    // ignore
                  }
                }
              }
            })
        .start();

    // thread 2
    new Thread(
            new Runnable() {
              public void run() {
                try {
                  log.info("Thread-2: sleep 0.5sec");
                  sleep(500);
                  log.info("Thread-2: try to get PLock...");
                  PLock p1 = pessimisticLockManager.findOrPersistPLock(asset);
                  assertNotNull(p1);
                  log.info("Thread-2: got PLock p1=" + p1);
                  log.info("Thread-2: sleep 1sec");
                  sleep(1000);
                  PLock p2 = pessimisticLockManager.findOrPersistPLock(asset);
                  assertNotNull(p2);
                  log.info("Thread-1: got PLock p2=" + p2);
                  log.info("Thread-1: finished");
                } catch (Exception e) {
                  exceptionHolder.add(e);
                } finally {
                  finishCount.countDown();
                  try {
                    dbInstance.commitAndCloseSession();
                  } catch (Exception e) {
                    // ignore
                  }
                }
              }
            })
        .start();

    // sleep until t1 and t2 should have terminated/excepted
    try {
      finishCount.await(60, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      Assert.fail("Test takes too long (more than 60s)");
    }

    // if not -> they are in deadlock and the db did not detect it
    for (Exception exception : exceptionHolder) {
      log.info("exception: " + exception.getMessage());
      exception.printStackTrace();
    }
    assertTrue("exception in test => see sysout", exceptionHolder.size() == 0);
  }

  @Test
  public void testLockWaitTimout() {
    // Ignore Test if DB is PostgreSQL. PostgreSQL has not lock timeout
    assumeTrue(!isPostgresqlConfigured() && !isOracleConfigured());

    final String asset = "testLockWaitTimout";

    log.info("testing if holding a lock timeouts");
    // make sure all three row entries for the locks are created, otherwise the system-wide locking
    // applied on lock-row-creation cannot support row-level-locking by definition.

    PLock pc3 = pessimisticLockManager.findOrPersistPLock("blibli");
    assertNotNull(pc3);
    dbInstance.closeSession();

    /** t1 t2 .. bli .. .. .. .. .. .. bli .. .. .. .... hold for longer than 30 secs */
    final List<Exception> exceptionHolder =
        Collections.synchronizedList(new ArrayList<Exception>(1));
    final CountDownLatch finishCount = new CountDownLatch(2);

    // t1
    new Thread(
            new Runnable() {
              public void run() {
                try {
                  sleep(500);
                  PLock p3 = pessimisticLockManager.findOrPersistPLock(asset);
                  assertNotNull(p3);
                } catch (Exception e) {
                  exceptionHolder.add(e);
                } finally {
                  finishCount.countDown();
                  try {
                    dbInstance.closeSession();
                  } catch (Exception e) {
                    // ignore
                  }
                }
              }
            })
        .start();

    // t2
    new Thread(
            new Runnable() {
              public void run() {
                try {
                  PLock p2 = pessimisticLockManager.findOrPersistPLock(asset);
                  assertNotNull(p2);
                  sleep(55000);
                  // holding the lock for more than the transaction timeout
                  // (normally 30secs, configured where? hib) should cause a lock timeout
                  // if the db is configured so (innodb_lock_wait_timeout).
                } catch (Exception e) {
                  exceptionHolder.add(e);
                } finally {
                  finishCount.countDown();
                  try {
                    dbInstance.closeSession();
                  } catch (Exception e) {
                    // ignore
                  }
                }
              }
            })
        .start();

    // sleep until t1 and t2 should have terminated/excepted
    try {
      log.info("Sleep 55s");
      finishCount.await(60, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      Assert.fail("");
    }

    Assert.assertEquals(
        "expected a lock wait timeout exceeded exception", 1, exceptionHolder.size());
  }

  @Test
  public void testSingleRowLockingSupported() {
    log.info(
        "testing if one lock only locks the given row and not the complete table (test whether the database supports rowlocking)");
    // make sure both row entries for the locks are created, otherwise the system-wide locking
    // applied on lock-row-creation cannot support row-level-locking by definition.
    PLock pc1 = pessimisticLockManager.findOrPersistPLock("blabla");
    Assert.assertNotNull(pc1);
    PLock pc2 = pessimisticLockManager.findOrPersistPLock("blublu");
    Assert.assertNotNull(pc2);
    dbInstance.closeSession();

    final List<Long> holder = new ArrayList<Long>(1);
    // first thread acquires the lock and waits and continues holding the lock for some time.
    PLock p1 = pessimisticLockManager.findOrPersistPLock("blabla");
    Assert.assertNotNull(p1);

    new Thread(
            new Runnable() {
              public void run() {
                PLock p2 = pessimisticLockManager.findOrPersistPLock("blublu");
                assertNotNull(p2);
                long p2Acquired = System.nanoTime();
                holder.add(new Long(p2Acquired));
                dbInstance.closeSession();
              }
            })
        .start();

    sleep(500);
    long p1AboutToRelease = System.nanoTime();
    dbInstance.closeSession();

    // if row locking is not supported, then the timestamp when p2 has been acquired will be shortly
    // -after- p1 has been released
    boolean singleRowLockingOk = holder.size() > 0 && holder.get(0).longValue() < p1AboutToRelease;
    assertTrue(
        "the database does not seem to support row locking when executing 'select for update', critical for performance!, ",
        singleRowLockingOk);
  }

  @Test
  public void testNestedLockingSupported() {
    log.info("testing if nested locking is supported");
    // make sure all three row entries for the locks are created, otherwise the system-wide locking
    // applied on lock-row-creation cannot support row-level-locking by definition.

    PLock pc1 = pessimisticLockManager.findOrPersistPLock("blabla");
    assertNotNull(pc1);
    PLock pc2 = pessimisticLockManager.findOrPersistPLock("blublu");
    assertNotNull(pc2);
    PLock pc3 = pessimisticLockManager.findOrPersistPLock("blibli");
    assertNotNull(pc3);
    dbInstance.closeSession();

    final List<Long> holder = new ArrayList<Long>(1);
    // first thread acquires the two locks and waits and continues holding the lock for some time.
    PLock p1 = pessimisticLockManager.findOrPersistPLock("blabla");
    assertNotNull(p1);
    PLock p3 = pessimisticLockManager.findOrPersistPLock("blibli");
    assertNotNull(p3);

    new Thread(
            new Runnable() {
              public void run() {
                PLock p2 = pessimisticLockManager.findOrPersistPLock("blibli");
                assertNotNull(p2);
                long p2Acquired = System.nanoTime();
                holder.add(new Long(p2Acquired));
                dbInstance.closeSession();
              }
            })
        .start();
    sleep(500);
    boolean acOk = holder.size() == 0;
    // the commit will drop the lock on blibli d
    dbInstance.closeSession();
    sleep(500);
    boolean acNowOk = holder.size() == 1;

    // if row locking is not supported, then the timestamp when p2 has been acquired will be shortly
    // -after- p1 has been released
    assertTrue("since holding the blabla lock, no other may acquire it", acOk);
    assertTrue(
        "after having released the blabla lock, a next waiting thread must have acquired it after some time",
        acNowOk);
  }

  @Test
  public void testDeadLockTimeout() {
    log.info("testing if deadlock detection and handling is supported");
    // make sure all three row entries for the locks are created, otherwise the system-wide locking
    // applied on lock-row-creation cannot support row-level-locking by definition.

    PLock pc1 = pessimisticLockManager.findOrPersistPLock("blabla");
    assertNotNull(pc1);
    PLock pc2 = pessimisticLockManager.findOrPersistPLock("blublu");
    assertNotNull(pc2);
    PLock pc3 = pessimisticLockManager.findOrPersistPLock("blibli");
    assertNotNull(pc3);
    dbInstance.closeSession();

    /**
     * t1 t2 bla bli .. .. .. .. .. .. bli .. .. .. bla -> deadlock! t2 waits on bla (already
     * acquired by t1, but t1 waits on bli, already acquired by t2)
     */
    final List<Exception> exceptionHolder =
        Collections.synchronizedList(new ArrayList<Exception>(1));
    final CountDownLatch finishCount = new CountDownLatch(2);
    // t1
    new Thread(
            new Runnable() {
              public void run() {
                try {
                  PLock p1 = pessimisticLockManager.findOrPersistPLock("blabla");
                  assertNotNull(p1);
                  sleep(250);
                  // now try to acquire blibli but that fails, since blibli is already locked by
                  // thread 2.
                  // but thread 2 cannot continue either, since it is waiting for lock blabla, which
                  // is already hold by thread 1
                  // -> deadlock
                  PLock p3 = pessimisticLockManager.findOrPersistPLock("blibli");
                  assertNotNull(p3);
                } catch (Exception e) {
                  exceptionHolder.add(e);
                } finally {
                  try {
                    dbInstance.closeSession();
                  } catch (Exception e) {
                    // ignore
                  }
                  finishCount.countDown();
                }
              }
            })
        .start();

    // t2
    new Thread(
            new Runnable() {
              public void run() {
                try {
                  PLock p2 = pessimisticLockManager.findOrPersistPLock("blibli");
                  assertNotNull(p2);
                  sleep(500);
                  PLock p3 = pessimisticLockManager.findOrPersistPLock("blabla");
                  assertNotNull(p3);
                } catch (Exception e) {
                  exceptionHolder.add(e);
                } finally {
                  try {
                    dbInstance.closeSession();
                  } catch (Exception e) {
                    // ignore
                  }
                  finishCount.countDown();
                }
              }
            })
        .start();

    // sleep until t1 and t2 should have terminated/excepted
    try {
      finishCount.await(8, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      Assert.fail("Takes too long (more than 8sec)");
    }

    // if not -> they are in deadlock and the db did not detect it
    for (Exception exception : exceptionHolder) {
      log.error("exception: ", exception);
    }
    assertTrue("expected a deadlock exception, but got none", exceptionHolder.size() > 0);
  }

  @Test
  public void testPerf() {
    log.info("testing what the throughput is for the pessimistic locking");
    // test what the throughput is for the pessimistic locking.
    // take 500 threads (created and started with no delay (as fast as the vm can) trying to acquire
    // a plock on 20 different olatresourceables.
    // measure how long that takes. and warn if it exceeds an upper boundary.
    // the server is assumed to have around 2GHz cpu and 2GBytes RAM.
    // the first thread to acquire a new olatres will first lock on the global lock and then create
    // the new entry to lock upon.
    // we therefore also measure how long it takes again when all locks have already been inserted
    // into the plock table.
    // results: on a std. laptop with postgres 8, 500 threads with 20 resourceables take about
    // 3000ms (thread creation inclusive)
    // -> about

    // 1. prepare collection
    int numthreads = 500;
    int numores = 1;

    // 2. create 500 threads and start them
    long start = System.currentTimeMillis();
    final CountDownLatch doneSignal = new CountDownLatch(numthreads);
    for (int i = 0; i < numthreads; i++) {
      final String asset = "assetaboutaslongasores" + (i % numores);
      Runnable r =
          new Runnable() {
            public void run() {
              try {
                pessimisticLockManager.findOrPersistPLock(asset);
                doneSignal.countDown();
              } catch (Exception e) {
                e.printStackTrace();
              } finally {
                dbInstance.closeSession();
              }
            }
          };
      new Thread(r).start();
    }

    // 4. wait till all are finished or it takes too long
    try {
      doneSignal.await(20, TimeUnit.SECONDS);
      log.info("perf for Plocktest:testPerf(): " + (System.currentTimeMillis() - start));
    } catch (InterruptedException e) {
      fail("Test takes too long (more than 20s)");
    }

    // repeat the same again - this time it should/could be faster
    // 2. create 500 threads and start them
    long start2 = System.currentTimeMillis();
    final CountDownLatch doneSignal2 = new CountDownLatch(numthreads);
    for (int i2 = 0; i2 < numthreads; i2++) {
      final String asset = "assetaboutaslongasores" + (i2 % numores);
      Runnable r =
          new Runnable() {
            public void run() {
              try {
                pessimisticLockManager.findOrPersistPLock(asset);
                doneSignal2.countDown();
              } catch (Exception e) {
                e.printStackTrace();
              } finally {
                dbInstance.commitAndCloseSession();
              }
            }
          };
      new Thread(r).start();
    }

    // 4. wait till all are finished or it takes too long

    try {
      boolean interrupt = doneSignal.await(20, TimeUnit.SECONDS);
      log.info("perf (again) for Plocktest:testPerf(): " + (System.currentTimeMillis() - start2));
      assertTrue("Test takes too long (more than 20s)", interrupt);
    } catch (InterruptedException e) {
      fail("" + e.getMessage());
    }
  }

  @Test
  public void testSync() {
    log.info("testing enrollment");
    //	 ------------------ now check with lock -------------------
    // create a group
    //	 create users
    final List<Identity> identities = new ArrayList<Identity>();
    for (int i = 0; i < MAX_COUNT + MAX_USERS_MORE; i++) {
      Identity id =
          JunitTestHelper.createAndPersistIdentityAsUser(
              "u-" + i + "-" + UUID.randomUUID().toString());
      identities.add(id);
      log.info("testSync: Identity=" + id.getName() + " created");
    }
    dbInstance.closeSession();

    final SecurityGroup group2 = BaseSecurityManager.getInstance().createAndPersistSecurityGroup();
    // make sure the lock has been written to the disk (tests for createOrFind see other methods)
    dbInstance.closeSession();

    // prepare threads
    int numOfThreads = MAX_COUNT + MAX_USERS_MORE;
    final CountDownLatch finishCount = new CountDownLatch(numOfThreads);

    // try to enrol all in the same group
    for (int i = 0; i < numOfThreads; i++) {
      final int j = i;
      new Thread(
              new Runnable() {
                public void run() {
                  try {
                    log.info("testSync: thread started j=" + j);
                    Identity id = identities.get(j);
                    //
                    PLock p2 = pessimisticLockManager.findOrPersistPLock("befinsert");
                    assertNotNull(p2);
                    doNoLockingEnrol(id, group2);
                    dbInstance.commit();
                    dbInstance.closeSession();
                  } catch (Exception e) {
                    e.printStackTrace();
                  } finally {
                    finishCount.countDown();
                  }
                }
              })
          .start();
    }

    try {
      finishCount.await(120, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
      log.error("", e);
    }

    // now count
    dbInstance.closeSession();
    int cnt2 = BaseSecurityManager.getInstance().countIdentitiesOfSecurityGroup(group2);
    assertTrue(
        "cnt should be smaller or eq than allowed since synced with select for update. cnt:"
            + cnt2
            + ", max "
            + MAX_COUNT,
        cnt2 <= MAX_COUNT);
    assertTrue(
        "cnt should be eq to allowed since synced with select for update. cnt:"
            + cnt2
            + ", max "
            + MAX_COUNT,
        cnt2 == MAX_COUNT);
    log.info("cnt lock " + cnt2);
  }

  private void doNoLockingEnrol(Identity i, SecurityGroup group) {
    // check that below max
    try {
      StringBuilder sb = new StringBuilder();
      int cnt = BaseSecurityManager.getInstance().countIdentitiesOfSecurityGroup(group);
      sb.append("enrol:cnt:" + cnt);
      if (cnt < MAX_COUNT) {
        // now sleep a while to allow others to think also that there is still space left in the
        // group
        sleep(100);
        // now add the user to the security group
        sb.append(" adding " + i.getName() + ": current.. " + cnt + ", max = " + MAX_COUNT);
        BaseSecurityManager.getInstance().addIdentityToSecurityGroup(i, group);
      }
      log.info(sb.toString());
    } catch (Exception e) {
      log.error("", e);
    }
  }
}
Example #13
0
  /**
   * Update the module configuration to have all mandatory configuration flags set to usefull
   * default values
   *
   * @param isNewNode true: an initial configuration is set; false: upgrading from previous node
   *     configuration version, set default to maintain previous behaviour
   */
  @Override
  public void updateModuleConfigDefaults(boolean isNewNode) {
    int CURRENTVERSION = 7;
    ModuleConfiguration config = getModuleConfiguration();
    if (isNewNode) {
      // use defaults for new course building blocks
      config.setBooleanEntry(NodeEditController.CONFIG_STARTPAGE, Boolean.FALSE.booleanValue());
      config.setBooleanEntry(NodeEditController.CONFIG_COMPONENT_MENU, Boolean.TRUE.booleanValue());
      // how to render files (include jquery etc)
      DeliveryOptions nodeDeliveryOptions = DeliveryOptions.defaultWithGlossary();
      nodeDeliveryOptions.setInherit(Boolean.TRUE);
      config.set(CPEditController.CONFIG_DELIVERYOPTIONS, nodeDeliveryOptions);
      config.setConfigurationVersion(CURRENTVERSION);
    } else {
      config.remove(NodeEditController.CONFIG_INTEGRATION);
      if (config.getConfigurationVersion() < 2) {
        // update new configuration options using default values for existing
        // nodes
        config.setBooleanEntry(NodeEditController.CONFIG_STARTPAGE, Boolean.TRUE.booleanValue());
        Boolean componentMenu = config.getBooleanEntry(NodeEditController.CONFIG_COMPONENT_MENU);
        if (componentMenu == null) {
          config.setBooleanEntry(
              NodeEditController.CONFIG_COMPONENT_MENU, Boolean.TRUE.booleanValue());
        }
        config.setConfigurationVersion(2);
      }

      if (config.getConfigurationVersion() < 3) {
        config.set(
            NodeEditController.CONFIG_CONTENT_ENCODING,
            NodeEditController.CONFIG_CONTENT_ENCODING_AUTO);
        config.set(
            NodeEditController.CONFIG_JS_ENCODING, NodeEditController.CONFIG_JS_ENCODING_AUTO);
        config.setConfigurationVersion(3);
      }
      // Version 5 was ineffective since the delivery options were not set. We have to redo this and
      // save it as version 6
      if (config.getConfigurationVersion() < 7) {
        String contentEncoding = (String) config.get(NodeEditController.CONFIG_CONTENT_ENCODING);
        if (contentEncoding != null && contentEncoding.equals("auto")) {
          contentEncoding = null; // new style for auto
        }
        String jsEncoding = (String) config.get(NodeEditController.CONFIG_JS_ENCODING);
        if (jsEncoding != null && jsEncoding.equals("auto")) {
          jsEncoding = null; // new style for auto
        }

        CPPackageConfig reConfig = null;
        DeliveryOptions nodeDeliveryOptions =
            (DeliveryOptions) config.get(CPEditController.CONFIG_DELIVERYOPTIONS);
        if (nodeDeliveryOptions == null) {
          // Update missing delivery options now, inherit from repo by default
          nodeDeliveryOptions = DeliveryOptions.defaultWithGlossary();
          nodeDeliveryOptions.setInherit(Boolean.TRUE);

          RepositoryEntry re = getReferencedRepositoryEntry();
          // Check if delivery options are set for repo entry, if not create default
          if (re != null) {
            reConfig = CPManager.getInstance().getCPPackageConfig(re.getOlatResource());
            if (reConfig == null) {
              reConfig = new CPPackageConfig();
            }
            DeliveryOptions repoDeliveryOptions = reConfig.getDeliveryOptions();
            if (repoDeliveryOptions == null) {
              // migrate existing config back to repo entry using the default as a base
              repoDeliveryOptions = DeliveryOptions.defaultWithGlossary();
              reConfig.setDeliveryOptions(repoDeliveryOptions);
              repoDeliveryOptions.setContentEncoding(contentEncoding);
              repoDeliveryOptions.setJavascriptEncoding(jsEncoding);
              CPManager.getInstance().setCPPackageConfig(re.getOlatResource(), reConfig);
            } else {
              // see if we have any different settings than the repo. if so, don't use inherit mode
              if (contentEncoding != repoDeliveryOptions.getContentEncoding()
                  || jsEncoding != repoDeliveryOptions.getJavascriptEncoding()) {
                nodeDeliveryOptions.setInherit(Boolean.FALSE);
                nodeDeliveryOptions.setContentEncoding(contentEncoding);
                nodeDeliveryOptions.setJavascriptEncoding(jsEncoding);
              }
            }
          }
          // remove old config parameters
          config.remove(NodeEditController.CONFIG_CONTENT_ENCODING);
          config.remove(NodeEditController.CONFIG_JS_ENCODING);
          // replace with new delivery options
          config.set(CPEditController.CONFIG_DELIVERYOPTIONS, nodeDeliveryOptions);
        }
        config.setConfigurationVersion(7);
      }

      // else node is up-to-date - nothing to do
    }
    if (config.getConfigurationVersion() != CURRENTVERSION) {
      OLog logger = Tracing.createLoggerFor(CPCourseNode.class);
      logger.error(
          "CP course node version not updated to lastest version::"
              + CURRENTVERSION
              + ", was::"
              + config.getConfigurationVersion()
              + ". Check the code, programming error.");
    }
  }
Example #14
0
/**
 * Initial Date: Mar 11, 2004
 *
 * @author Mike Stock
 *     <p>Comment:
 */
public class PropertyTest extends OlatTestCase {

  private static final OLog log = Tracing.createLoggerFor(PropertyTest.class);

  @Autowired private DB dbInstance;
  @Autowired private PropertyManager pm;
  @Autowired private BusinessGroupService businessGroupService;

  /** testGenericInsertFindDelete */
  @Test
  public void testGenericInsertFindDelete() {
    // create resource, identity and group
    OLATResource ores = JunitTestHelper.createRandomResource();
    Identity identity =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-1-" + UUID.randomUUID().toString());
    BusinessGroup group =
        businessGroupService.createBusinessGroup(
            identity, "a buddygroup", "a desc", -1, -1, false, false, null);
    dbInstance.commitAndCloseSession();

    Property p =
        pm.createPropertyInstance(
            identity,
            group,
            ores,
            "catgeneric",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    dbInstance.commitAndCloseSession();

    p = pm.findProperty(identity, group, ores, "catgeneric", "TestProperty");
    assertNotNull(p);
    assertEquals(p.getStringValue(), "stringValue");
    assertEquals(p.getFloatValue(), new Float(1.1));
    assertEquals(p.getLongValue(), new Long(123456));
    assertEquals(p.getTextValue(), "textValue");

    pm.deleteProperty(p);
    p = pm.findProperty(identity, group, ores, "catgeneric", "TestProperty");
    assertNull(p);
  }

  @Test
  public void testFindWithResourceIdList() {
    // create resource, identity
    OLATResource ores = JunitTestHelper.createRandomResource();
    Identity identity =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-2-" + UUID.randomUUID().toString());
    dbInstance.commitAndCloseSession();
    // create the property
    Property p =
        pm.createPropertyInstance(
            identity,
            null,
            ores,
            "catidlist",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    dbInstance.commitAndCloseSession();

    // check with empty list
    List<Property> emptyProps =
        pm.findProperties(
            ores.getResourceableTypeName(),
            Collections.<Long>emptyList(),
            "catidlist",
            "TestProperty");
    assertNotNull(emptyProps);
    Assert.assertTrue(emptyProps.isEmpty());

    // check with a real value and a dummy
    List<Long> resIds = new ArrayList<Long>();
    resIds.add(ores.getResourceableId());
    resIds.add(2456l); // dummy
    List<Property> props =
        pm.findProperties(ores.getResourceableTypeName(), resIds, "catidlist", "TestProperty");
    assertNotNull(props);
    Assert.assertEquals(1, props.size());
    Assert.assertEquals(p, props.get(0));
  }

  @Test
  public void testFindWithIdentityList() {
    // create identities, resource and properties
    Identity id1 =
        JunitTestHelper.createAndPersistIdentityAsUser("cat-id-1-" + UUID.randomUUID().toString());
    Identity id2 =
        JunitTestHelper.createAndPersistIdentityAsUser("cat-id-2-" + UUID.randomUUID().toString());
    Identity id3 =
        JunitTestHelper.createAndPersistIdentityAsUser("cat-id-3-" + UUID.randomUUID().toString());
    OLATResource ores = JunitTestHelper.createRandomResource();
    Property p1 =
        pm.createPropertyInstance(
            id1,
            null,
            ores,
            "catidentlist",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p1);
    Property p2 =
        pm.createPropertyInstance(
            id2,
            null,
            ores,
            "catidentlist",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p2);
    Property p3 =
        pm.createPropertyInstance(
            id3,
            null,
            ores,
            "catidentlist",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p3);
    dbInstance.commitAndCloseSession();

    // check with empty list
    List<Property> emptyProps =
        pm.findProperties(Collections.<Identity>emptyList(), ores, "catidentlist", "TestProperty");
    assertNotNull(emptyProps);
    Assert.assertTrue(emptyProps.isEmpty());

    // check with a real value and a dummy
    List<Identity> identities = new ArrayList<Identity>();
    identities.add(id1);
    identities.add(id2);

    List<Property> props = pm.findProperties(identities, ores, "catidentlist", "TestProperty");
    assertNotNull(props);
    Assert.assertEquals(2, props.size());
    Assert.assertTrue(props.contains(p1));
    Assert.assertTrue(props.contains(p2));
  }

  /** testGetAllResourceTypeNames */
  @Test
  public void testGetAllResourceTypeNames() {
    // create resource, identity and group
    OLATResource ores = JunitTestHelper.createRandomResource();
    Identity identity =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-3-" + UUID.randomUUID().toString());
    BusinessGroup group =
        businessGroupService.createBusinessGroup(
            identity, "a buddygroup", "a desc", -1, -1, false, false, null);
    dbInstance.commitAndCloseSession();

    // create a property
    Property p =
        pm.createPropertyInstance(
            identity,
            group,
            ores,
            "catall",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    dbInstance.commitAndCloseSession();

    Assert.assertNotNull(p.getResourceTypeName());
    List<String> resTypeNames = pm.getAllResourceTypeNames();
    assertTrue(resTypeNames.contains(ores.getResourceableTypeName()));
  }

  /** testListProperties */
  @Test
  public void testListProperties() {
    // create resource, identity and group
    OLATResource ores = JunitTestHelper.createRandomResource();
    Identity identity =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-4-" + UUID.randomUUID().toString());
    BusinessGroup group =
        businessGroupService.createBusinessGroup(
            identity, "a buddygroup", "a desc", -1, -1, false, false, null);
    dbInstance.commitAndCloseSession();

    Property p =
        pm.createPropertyInstance(
            identity,
            group,
            ores,
            "cat",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);

    List<Property> entries =
        PropertyManager.getInstance()
            .listProperties(
                identity,
                group,
                ores.getResourceableTypeName(),
                ores.getResourceableId(),
                "cat",
                "TestProperty");
    Assert.assertNotNull(entries);
    Assert.assertEquals(1, entries.size());

    Property prop = entries.get(0);
    assertEquals(ores.getResourceableTypeName(), prop.getResourceTypeName());
    assertEquals(ores.getResourceableId(), prop.getResourceTypeId());

    int numOfEntries =
        PropertyManager.getInstance()
            .countProperties(
                identity,
                group,
                ores.getResourceableTypeName(),
                ores.getResourceableId(),
                "cat",
                "TestProperty",
                null,
                null);
    Assert.assertEquals(entries.size(), numOfEntries);
  }

  /** testUserInsertFindDelete */
  @Test
  public void testUserInsertFindDelete() {
    // create identity and property
    Identity id =
        JunitTestHelper.createAndPersistIdentityAsUser("user-prop-" + UUID.randomUUID().toString());
    Property p =
        pm.createUserPropertyInstance(
            id,
            "catuser",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    dbInstance.commitAndCloseSession();

    p = pm.findUserProperty(id, "catuser", "TestProperty");
    assertNotNull(p);
    assertEquals(p.getStringValue(), "stringValue");
    assertEquals(p.getFloatValue(), new Float(1.1));
    assertEquals(p.getTextValue(), "textValue");

    pm.deleteProperty(p);
    p = pm.findUserProperty(id, "catuser", "TestProperty");
    assertNull(p);
    dbInstance.commitAndCloseSession();
  }

  @Test
  public void testDeleteUserData() {
    // create some identities and properties
    Identity id1 =
        JunitTestHelper.createAndPersistIdentityAsUser(
            "del-user-1-" + UUID.randomUUID().toString());
    Identity id2 =
        JunitTestHelper.createAndPersistIdentityAsUser(
            "del-user-2-" + UUID.randomUUID().toString());
    Property p10 =
        pm.createPropertyInstance(
            id1,
            null,
            null,
            "prop-del-1",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p10);
    Property p11 =
        pm.createPropertyInstance(
            id1,
            null,
            null,
            "prop-del-2",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p11);
    Property p20 =
        pm.createPropertyInstance(
            id2,
            null,
            null,
            "prop-del-3",
            "TestProperty",
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p20);
    dbInstance.commitAndCloseSession();

    // delete user 1 datas
    pm.deleteUserData(id1, "del-" + id1.getName(), null);
    dbInstance.commitAndCloseSession();

    // check if really deleted
    Property p10x = pm.findUserProperty(id1, "prop-del-1", "TestProperty");
    assertNull(p10x);
    Property p11x = pm.findUserProperty(id1, "prop-del-2", "TestProperty");
    assertNull(p11x);

    // check if id2 has still its properties
    Property p20x = pm.findUserProperty(id2, "prop-del-3", "TestProperty");
    assertNotNull(p20x);
  }

  /**
   * Performance test of 500 propertycreations per type. Rename to testPerf500Properties to include
   * this test in the test suit.
   */
  @Test
  public void testPerf500Properties() {
    // create identity, group and resource
    OLATResource res = JunitTestHelper.createRandomResource();
    Identity identity =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-5-" + UUID.randomUUID().toString());
    BusinessGroup group =
        businessGroupService.createBusinessGroup(
            identity, "a buddygroup", "a desc", -1, -1, false, false, null);
    dbInstance.commitAndCloseSession();

    long start, stop;
    long count = 500;

    // create generic proerties
    log.info("----------------------------------------------------------------");
    log.info("Performance test startet. Running " + count + " cycles per test.");
    log.info("CREATE generic property test started...");
    start = System.currentTimeMillis();
    for (int i = 0; i < count; i++) {
      Property p =
          pm.createPropertyInstance(
              identity,
              group,
              res,
              "perf500",
              "TestProperty" + i,
              new Float(1.1),
              new Long(123456),
              "stringValue",
              "textValue");
      pm.saveProperty(p);

      if (i % 50 == 0) {
        dbInstance.commitAndCloseSession();
      }
    }
    dbInstance.commitAndCloseSession();

    stop = System.currentTimeMillis();
    log.info(
        "CREATE generic property test: "
            + (stop - start)
            + " ms ("
            + (count * 1000 / (stop - start))
            + "/sec)");
    // some find identitites tests
    List<Identity> ids = pm.findIdentitiesWithProperty(null, null, "perf500", null, false);
    Assert.assertNotNull("Identities cannot be null", ids);
    Assert.assertFalse("Identities cannot be empty", ids.isEmpty());
    Assert.assertTrue("Identities must contains reference identity", ids.contains(identity));

    // create course and user properties
    log.info("Preparing user/group properties test. Creating additional properties..");
    start = System.currentTimeMillis();
    for (int i = 0; i < count; i++) {
      Property pUser =
          pm.createUserPropertyInstance(
              identity,
              "perf500",
              "TestProperty" + i,
              new Float(1.1),
              new Long(123456),
              "stringValue",
              "textValue");
      pm.saveProperty(pUser);
      if (i % 50 == 0) {
        dbInstance.commitAndCloseSession();
      }
    }
    dbInstance.commitAndCloseSession();

    stop = System.currentTimeMillis();
    log.info("Ready : " + (stop - start) + " ms (" + (2 * count * 1000 / (stop - start)) + "/sec)");
    log.info("Starting find tests. DB holds " + count * 3 + " records.");

    // find generic property test
    log.info("FIND generic property test started...");
    start = System.currentTimeMillis();
    for (int i = 0; i < count; i++) {
      Property p = pm.findProperty(identity, group, res, "perf500", "TestProperty" + i);
      assertNotNull("Must find the p (count=" + i + ")", p);
      dbInstance.commitAndCloseSession();
    }
    stop = System.currentTimeMillis();
    log.info(
        "FIND generic property test: "
            + (stop - start)
            + " ms ("
            + (count * 1000 / (stop - start))
            + "/sec)");

    // find user property test
    log.info("FIND user property test started...");
    start = System.currentTimeMillis();
    for (int i = 0; i < count; i++) {
      Property p = pm.findUserProperty(identity, "perf500", "TestProperty" + i);
      assertNotNull("Must find the p (count=" + i + ")", p);
      dbInstance.commitAndCloseSession();
    }
    stop = System.currentTimeMillis();
    log.info(
        "FIND user property test: "
            + (stop - start)
            + " ms ("
            + (count * 1000 / (stop - start))
            + "/sec)");

    // find & delete
    log.info("FIND and DELETE generic property test started...");
    start = System.currentTimeMillis();
    for (int i = 0; i < count; i++) {
      Property p = pm.findUserProperty(identity, "perf500", "TestProperty" + i);
      pm.deleteProperty(p);
    }
    stop = System.currentTimeMillis();
    log.info(
        "FIND and DELETE generic property test: "
            + (stop - start)
            + " ms ("
            + (count * 1000 / (stop - start))
            + "/sec)");

    log.info("----------------------------------------------------------------");
    log.info("Performance test finished.");
  }

  /**
   * testFloatValues THIS test does only success when you run it against mysql with FLOAT(65,30).
   * FLOAT(65,30) is mysql specific and if you let hibernate generate the tables automatic it will
   * result in FLOAT only and this test will fail. So this means that you have to populate the
   * tables yourself via the sql file.
   */
  @Test
  public void testFloatValues() {
    // create identity, group and resource
    OLATResource res = JunitTestHelper.createRandomResource();
    Identity identity =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-6-" + UUID.randomUUID().toString());
    BusinessGroup group =
        businessGroupService.createBusinessGroup(
            identity, "a buddygroup", "a desc", -1, -1, false, false, null);
    dbInstance.commitAndCloseSession();

    Property original, copy;
    // gs:changed to FLOAT(65,30) to be compatible with hsqldb and auto generated ddl
    // Define my own MAX float value because the db precision changed from DECIMAL(78,36) to
    // DECIMAL(65,30)
    double floatMaxValue = 1E34; // 1E35 does failed with mysql 5.0.x
    original =
        pm.createPropertyInstance(
            identity,
            group,
            res,
            "cat",
            "TestProperty",
            new Float(1234534343424213.1324534533456),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(original);
    // DBFactory.getInstance().evict(original);
    dbInstance.commitAndCloseSession();
    copy = pm.findProperty(identity, group, res, "cat", "TestProperty");
    float f1F = original.getFloatValue().floatValue();
    float copyF = copy.getFloatValue().floatValue();
    assertEquals("values differ:" + f1F + ", and " + copyF, f1F, copyF, 0.0000000001f);
    pm.deleteProperties(identity, group, res, "cat", "TestProperty");

    // note: on mysql 5.0, the standard installation is strict mode, which reports any data
    // truncation error as a real jdbc error.
    // -1e35 seems out of range for DECIMAL(65,30) so data truncation occurs???
    //                                          +-> 30 digits to the right side of decimal point
    // From mysql:
    // The declaration syntax for a DECIMAL column is DECIMAL(M,D). The ranges of values for the
    // arguments in MySQL 5.1 are as follows:
    // M is the maximum number of digits (the precision). It has a range of 1 to 65. (Older versions
    // of MySQL allowed a range of 1 to 254.)
    // D is the number of digits to the right of the decimal point (the scale). It has a range of 0
    // to 30 and must be no larger than M.
    original =
        pm.createPropertyInstance(
            identity,
            group,
            res,
            "cat",
            "TestProperty",
            new Float(-floatMaxValue),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(original);
    dbInstance.commitAndCloseSession();
    copy =
        pm.findProperty(
            identity,
            group,
            res,
            "cat",
            "TestProperty"); // this one failes at the moment for hsqldb with: incompatible data
                             // type in conversion: from SQL type DECIMAL to java.lang.Double,
                             // value:
                             // -9999999790214767953607394487959552.000000000000000000000000000000
    assertTrue(original.getFloatValue().floatValue() == copy.getFloatValue().floatValue());
    pm.deleteProperties(identity, group, res, "cat", "TestProperty");

    original =
        pm.createPropertyInstance(
            identity,
            group,
            res,
            "cat",
            "TestProperty",
            new Float(floatMaxValue),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(original);
    dbInstance.commitAndCloseSession();
    copy = pm.findProperty(identity, group, res, "cat", "TestProperty");
    assertTrue(original.getFloatValue().floatValue() == copy.getFloatValue().floatValue());
    pm.deleteProperties(identity, group, res, "cat", "TestProperty");

    original =
        pm.createPropertyInstance(
            identity,
            group,
            res,
            "cat",
            "TestProperty",
            new Float(Long.MAX_VALUE),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(original);
    dbInstance.commitAndCloseSession();
    copy = pm.findProperty(identity, group, res, "cat", "TestProperty");
    assertTrue(original.getFloatValue().floatValue() == copy.getFloatValue().floatValue());
    pm.deleteProperties(identity, group, res, "cat", "TestProperty");

    original =
        pm.createPropertyInstance(
            identity,
            group,
            res,
            "cat",
            "TestProperty",
            new Float(Long.MIN_VALUE),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(original);
    dbInstance.commitAndCloseSession();
    copy = pm.findProperty(identity, group, res, "cat", "TestProperty");
    assertTrue(original.getFloatValue().floatValue() == copy.getFloatValue().floatValue());
    pm.deleteProperties(identity, group, res, "cat", "TestProperty");
  }

  @Test
  public void testRealWorldScoreFloatValues() {
    // delete everything from previous tests
    pm.deleteProperties(null, null, null, "test", "TestPropertyFloatValue1");
    pm.deleteProperties(null, null, null, "test", "TestPropertyFloatValue2");
    // Test setting and getting of a normal float value
    Property prop1 =
        pm.createPropertyInstance(
            null, null, null, "test", "TestPropertyFloatValue1", new Float(0.9), null, null, null);
    pm.saveProperty(prop1);
    Property prop2 =
        pm.createPropertyInstance(
            null, null, null, "test", "TestPropertyFloatValue2", new Float(0.1), null, null, null);
    pm.saveProperty(prop2);
    dbInstance.commitAndCloseSession();
    prop1 = pm.findProperty(null, null, null, "test", "TestPropertyFloatValue1");
    prop2 = pm.findProperty(null, null, null, "test", "TestPropertyFloatValue2");
    // In course assessment 0.1 + 0.9 must exactly give 1 as a result
    assertEquals(
        1, prop1.getFloatValue().floatValue() + prop2.getFloatValue().floatValue(), 0.00001);
    pm.deleteProperties(null, null, null, "test", "TestPropertyFloatValue1");
    pm.deleteProperties(null, null, null, "test", "TestPropertyFloatValue2");
  }

  @Test
  public void testFindIdentities() {
    // create identities, group and resource
    OLATResource res = JunitTestHelper.createRandomResource();
    Identity id1 =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-7-" + UUID.randomUUID().toString());
    Identity id2 =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-10-" + UUID.randomUUID().toString());
    Identity id3 =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-11-" + UUID.randomUUID().toString());
    Identity id4 =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-12-" + UUID.randomUUID().toString());
    BusinessGroup group =
        businessGroupService.createBusinessGroup(
            id1, "a buddygroup", "a desc", -1, -1, false, false, null);
    dbInstance.commitAndCloseSession();

    String propName = UUID.randomUUID().toString();

    Property p =
        pm.createPropertyInstance(
            id1,
            group,
            res,
            "cat",
            propName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    p =
        pm.createPropertyInstance(
            id2,
            group,
            res,
            "cat",
            propName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    p =
        pm.createPropertyInstance(
            id3,
            group,
            res,
            "cat",
            propName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    p =
        pm.createPropertyInstance(
            id4,
            group,
            res,
            "cat",
            propName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    p =
        pm.createPropertyInstance(
            id1,
            group,
            res,
            "cat2",
            propName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);
    p =
        pm.createPropertyInstance(
            id2,
            group,
            res,
            "cat2",
            propName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            "textValue");
    pm.saveProperty(p);

    dbInstance.commitAndCloseSession();
    // now find identities
    List<Identity> ids = pm.findIdentitiesWithProperty(res, "cat", propName, false);
    assertEquals(4, ids.size());
    ids = pm.findIdentitiesWithProperty(res, "cat", propName, true);
    assertEquals(4, ids.size());
    ids = pm.findIdentitiesWithProperty(null, "cat", propName, false);
    assertEquals(4, ids.size());
    ids = pm.findIdentitiesWithProperty(null, "cat", propName, true);
    assertEquals(0, ids.size());
    ids = pm.findIdentitiesWithProperty(null, "cat2", propName, false);
    assertEquals(2, ids.size());
    ids = pm.findIdentitiesWithProperty(null, null, propName, false);
    assertEquals(4, ids.size()); // not 6, must be distinct
    ids = pm.findIdentitiesWithProperty(null, null, propName, true);
    assertEquals(0, ids.size());
  }

  @Test
  public void testFindProperties() {
    // create identities, group and resource
    OLATResource res = JunitTestHelper.createRandomResource();
    Identity id1 =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-8-" + UUID.randomUUID().toString());
    Identity id2 =
        JunitTestHelper.createAndPersistIdentityAsUser("prop-9-" + UUID.randomUUID().toString());
    BusinessGroup group =
        businessGroupService.createBusinessGroup(
            id1, "a buddygroup", "a desc", -1, -1, false, false, null);
    dbInstance.commitAndCloseSession();

    String category = "cat3";
    String propertyName = "TestProperty3";
    String textValue = "textValue3";
    Property p =
        pm.createPropertyInstance(
            id1,
            group,
            res,
            category,
            propertyName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            textValue);
    pm.saveProperty(p);
    p =
        pm.createPropertyInstance(
            id2,
            group,
            res,
            category,
            propertyName,
            new Float(1.1),
            new Long(123456),
            "stringValue",
            textValue);
    pm.saveProperty(p);
    List<Property> propertyList =
        pm.findProperties(
            id1,
            group,
            res.getResourceableTypeName(),
            res.getResourceableId(),
            category,
            propertyName);
    assertEquals(1, propertyList.size());
    assertEquals(propertyName, propertyList.get(0).getName());
    assertEquals(textValue, propertyList.get(0).getTextValue());
    int deletedCount1 = pm.deleteProperties(id1, group, res, category, propertyName);
    Assert.assertEquals(1, deletedCount1);
    int deletedCount2 = pm.deleteProperties(id2, group, res, category, propertyName);
    Assert.assertEquals(1, deletedCount2);
  }
}
/**
 * The extractor call an extern process: command pdf txt
 *
 * <p>Initial date: 19.07.2013<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
public class PdfExternalExtractor implements PdfExtractor {

  private static final OLog log = Tracing.createLoggerFor(PdfExternalExtractor.class);

  private SearchModule searchModule;

  /**
   * [used by Spring]
   *
   * @param searchModule
   */
  public void setSearchModule(SearchModule searchModule) {
    this.searchModule = searchModule;
  }

  @Override
  public void extract(VFSLeaf document, File bufferFile)
      throws IOException, DocumentAccessException {
    if (!(document instanceof LocalFileImpl)) {
      log.warn("Can only index local file");
      return;
    }

    List<String> cmds = new ArrayList<String>();
    cmds.add(searchModule.getPdfExternalIndexerCmd());
    cmds.add(((LocalFileImpl) document).getBasefile().getAbsolutePath());
    cmds.add(bufferFile.getAbsolutePath());

    CountDownLatch doneSignal = new CountDownLatch(1);

    ProcessWorker worker = new ProcessWorker(cmds, doneSignal);
    worker.start();

    try {
      doneSignal.await(3000, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
      log.error("", e);
    }

    worker.destroyProcess();
  }

  private final void executeProcess(Process proc) {
    StringBuilder errors = new StringBuilder();
    StringBuilder output = new StringBuilder();

    InputStream stderr = proc.getErrorStream();
    InputStreamReader iserr = new InputStreamReader(stderr);
    BufferedReader berr = new BufferedReader(iserr);
    String line = null;
    try {
      while ((line = berr.readLine()) != null) {
        errors.append(line);
      }
    } catch (IOException e) {
      //
    }

    InputStream stdout = proc.getInputStream();
    InputStreamReader isr = new InputStreamReader(stdout);
    BufferedReader br = new BufferedReader(isr);
    line = null;
    try {
      while ((line = br.readLine()) != null) {
        output.append(line);
      }
    } catch (IOException e) {
      //
    }

    try {
      int exitValue = proc.waitFor();
      if (log.isDebug()) {
        log.info("PDF extracted: " + exitValue);
      }
    } catch (InterruptedException e) {
      //
    }

    if (log.isDebug()) {
      log.error(errors.toString());
      log.info(output.toString());
    }
  }

  private class ProcessWorker extends Thread {

    private volatile Process process;

    private final List<String> cmd;
    private final CountDownLatch doneSignal;

    public ProcessWorker(List<String> cmd, CountDownLatch doneSignal) {
      this.cmd = cmd;
      this.doneSignal = doneSignal;
    }

    public void destroyProcess() {
      if (process != null) {
        process.destroy();
        process = null;
      }
    }

    @Override
    public void run() {

      try {
        if (log.isDebug()) {
          log.debug(cmd.toString());
        }

        ProcessBuilder builder = new ProcessBuilder(cmd);
        process = builder.start();
        executeProcess(process);
        doneSignal.countDown();
      } catch (IOException e) {
        log.error("Could not spawn convert sub process", e);
        destroyProcess();
      }
    }
  }
}
/**
 * Initial date: 12.12.2014<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
@Service("assessmentModeManager")
public class AssessmentModeManagerImpl implements AssessmentModeManager {

  private static final OLog log = Tracing.createLoggerFor(AssessmentModeManagerImpl.class);

  @Autowired private DB dbInstance;
  @Autowired private BGAreaManager areaMgr;
  @Autowired private AssessmentModeDAO assessmentModeDao;
  @Autowired private BusinessGroupRelationDAO businessGroupRelationDao;
  @Autowired private RepositoryEntryRelationDAO repositoryEntryRelationDao;
  @Autowired private AssessmentModeCoordinationServiceImpl assessmentModeCoordinationService;

  @Override
  public AssessmentMode createAssessmentMode(RepositoryEntry entry) {
    AssessmentModeImpl mode = new AssessmentModeImpl();
    mode.setCreationDate(new Date());
    mode.setLastModified(new Date());
    mode.setRepositoryEntry(entry);
    mode.setStatus(Status.none);
    mode.setManualBeginEnd(false);
    return mode;
  }

  protected Date evaluateLeadTime(Date begin, int leadtime) {
    Calendar cal = Calendar.getInstance();
    cal.setTime(begin);
    cal.set(Calendar.SECOND, 0);
    cal.set(Calendar.MILLISECOND, 0);
    if (leadtime > 0) {
      cal.add(Calendar.MINUTE, -leadtime);
    }
    return cal.getTime();
  }

  protected Date evaluateFollowupTime(Date end, int followupTime) {
    Calendar cal = Calendar.getInstance();
    cal.setTime(end);
    cal.set(Calendar.SECOND, 0);
    cal.set(Calendar.MILLISECOND, 0);
    if (followupTime > 0) {
      cal.add(Calendar.MINUTE, followupTime);
    }
    return cal.getTime();
  }

  @Override
  public AssessmentMode persist(AssessmentMode assessmentMode) {
    assessmentMode.setLastModified(new Date());

    // update begin with lead time
    Date begin = assessmentMode.getBegin();
    Date beginWithLeadTime = evaluateLeadTime(begin, assessmentMode.getLeadTime());
    ((AssessmentModeImpl) assessmentMode).setBeginWithLeadTime(beginWithLeadTime);
    Date end = assessmentMode.getEnd();
    Date endWithFollowupTime = this.evaluateFollowupTime(end, assessmentMode.getFollowupTime());
    ((AssessmentModeImpl) assessmentMode).setEndWithFollowupTime(endWithFollowupTime);

    dbInstance.getCurrentEntityManager().persist(assessmentMode);
    dbInstance.commit();
    return assessmentMode;
  }

  @Override
  public AssessmentMode merge(AssessmentMode assessmentMode, boolean forceStatus) {
    assessmentMode.setLastModified(new Date());

    // update begin with lead time
    Date begin = assessmentMode.getBegin();
    Date beginWithLeadTime = evaluateLeadTime(begin, assessmentMode.getLeadTime());
    ((AssessmentModeImpl) assessmentMode).setBeginWithLeadTime(beginWithLeadTime);

    Date end = assessmentMode.getEnd();
    Date endWithFollowupTime = evaluateFollowupTime(end, assessmentMode.getFollowupTime());
    ((AssessmentModeImpl) assessmentMode).setEndWithFollowupTime(endWithFollowupTime);

    AssessmentMode reloadedMode;
    if (assessmentMode.getKey() == null) {
      dbInstance.getCurrentEntityManager().persist(assessmentMode);
      reloadedMode = assessmentMode;
    } else {
      reloadedMode = dbInstance.getCurrentEntityManager().merge(assessmentMode);
    }
    dbInstance.commit();
    if (reloadedMode.isManualBeginEnd()) {
      reloadedMode =
          assessmentModeCoordinationService.syncManuallySetStatus(reloadedMode, forceStatus);
    } else {
      reloadedMode = assessmentModeCoordinationService.syncAutomicallySetStatus(reloadedMode);
    }
    return reloadedMode;
  }

  @Override
  public void delete(AssessmentMode assessmentMode) {
    assessmentModeDao.delete(assessmentMode);
  }

  @Override
  public AssessmentMode getAssessmentModeById(Long key) {
    return assessmentModeDao.getAssessmentModeById(key);
  }

  @Override
  public List<AssessmentMode> findAssessmentMode(SearchAssessmentModeParams params) {
    return assessmentModeDao.findAssessmentMode(params);
  }

  @Override
  public List<AssessmentMode> getAssessmentModeFor(RepositoryEntryRef entry) {
    return assessmentModeDao.getAssessmentModeFor(entry);
  }

  @Override
  public List<AssessmentMode> getAssessmentModeFor(IdentityRef identity) {
    List<AssessmentMode> currentModes = getAssessmentModes(new Date());
    List<AssessmentMode> myModes = null;
    if (currentModes.size() > 0) {
      // check permissions, groups, areas, course
      myModes = assessmentModeDao.loadAssessmentModeFor(identity, currentModes);
    }
    return myModes == null ? Collections.<AssessmentMode>emptyList() : myModes;
  }

  @Override
  public Set<Long> getAssessedIdentityKeys(AssessmentMode assessmentMode) {
    Target targetAudience = assessmentMode.getTargetAudience();
    RepositoryEntry re = assessmentMode.getRepositoryEntry();

    Set<Long> assessedKeys = new HashSet<>();
    if (targetAudience == Target.course || targetAudience == Target.courseAndGroups) {
      List<Long> courseMemberKeys =
          assessmentMode.isApplySettingsForCoach()
              ? repositoryEntryRelationDao.getMemberKeys(
                  re,
                  RepositoryEntryRelationType.defaultGroup,
                  GroupRoles.coach.name(),
                  GroupRoles.participant.name())
              : repositoryEntryRelationDao.getMemberKeys(
                  re, RepositoryEntryRelationType.defaultGroup, GroupRoles.participant.name());
      assessedKeys.addAll(courseMemberKeys);
    }
    if (targetAudience == Target.groups || targetAudience == Target.courseAndGroups) {
      List<BusinessGroup> groups = new ArrayList<>();

      Set<AssessmentModeToArea> modeToAreas = assessmentMode.getAreas();
      if (modeToAreas.size() > 0) {
        List<BGArea> areas = new ArrayList<>(modeToAreas.size());
        for (AssessmentModeToArea modeToArea : modeToAreas) {
          areas.add(modeToArea.getArea());
        }

        List<BusinessGroup> groupsInAreas = areaMgr.findBusinessGroupsOfAreas(areas);
        groups.addAll(groupsInAreas);
      }

      Set<AssessmentModeToGroup> modeToGroups = assessmentMode.getGroups();
      if (modeToGroups.size() > 0) {
        for (AssessmentModeToGroup modeToGroup : modeToGroups) {
          groups.add(modeToGroup.getBusinessGroup());
        }
      }

      List<Long> groupMemberKeys =
          assessmentMode.isApplySettingsForCoach()
              ? businessGroupRelationDao.getMemberKeys(
                  groups, GroupRoles.coach.name(), GroupRoles.participant.name())
              : businessGroupRelationDao.getMemberKeys(groups, GroupRoles.participant.name());
      assessedKeys.addAll(groupMemberKeys);
    }

    return assessedKeys;
  }

  @Override
  public List<AssessmentMode> getAssessmentModes(Date now) {
    return assessmentModeDao.getAssessmentModes(now);
  }

  @Override
  public boolean isInAssessmentMode(RepositoryEntryRef entry, Date date) {
    return assessmentModeDao.isInAssessmentMode(entry, date);
  }

  @Override
  public AssessmentModeToGroup createAssessmentModeToGroup(
      AssessmentMode mode, BusinessGroup group) {
    AssessmentModeToGroupImpl modeToGroup = new AssessmentModeToGroupImpl();
    modeToGroup.setAssessmentMode(mode);
    modeToGroup.setBusinessGroup(group);
    dbInstance.getCurrentEntityManager().persist(modeToGroup);
    return modeToGroup;
  }

  @Override
  public AssessmentModeToArea createAssessmentModeToArea(AssessmentMode mode, BGArea area) {
    AssessmentModeToAreaImpl modeToArea = new AssessmentModeToAreaImpl();
    modeToArea.setAssessmentMode(mode);
    modeToArea.setArea(area);
    dbInstance.getCurrentEntityManager().persist(modeToArea);
    return modeToArea;
  }

  @Override
  public boolean isNodeInUse(RepositoryEntryRef entry, CourseNode node) {
    return assessmentModeDao.isNodeInUse(entry, node);
  }

  @Override
  public boolean isIpAllowed(String ipList, String address) {
    boolean allOk = false;
    if (!StringHelper.containsNonWhitespace(ipList)) {
      allOk |= true;
    } else {
      for (StringTokenizer tokenizer = new StringTokenizer(ipList, "\n\r", false);
          tokenizer.hasMoreTokens(); ) {
        String ipRange = tokenizer.nextToken();
        if (StringHelper.containsNonWhitespace(ipRange)) {
          int indexMask = ipRange.indexOf("/");
          int indexPseudoRange = ipRange.indexOf("-");
          if (indexMask > 0) {
            allOk |= IPUtils.isValidRange(ipRange, address);
          } else if (indexPseudoRange > 0) {
            String begin = ipRange.substring(0, indexPseudoRange).trim();
            String end = ipRange.substring(indexPseudoRange + 1).trim();
            allOk |= IPUtils.isValidRange(begin, end, address);
          } else {
            allOk |= ipRange.equals(address);
          }
        }
      }
    }
    return allOk;
  }

  @Override
  public boolean isSafelyAllowed(HttpServletRequest request, String safeExamBrowserKeys) {
    boolean safe = false;
    boolean debug = log.isDebug();
    if (StringHelper.containsNonWhitespace(safeExamBrowserKeys)) {
      String safeExamHash = request.getHeader("x-safeexambrowser-requesthash");
      String url = request.getRequestURL().toString();
      for (StringTokenizer tokenizer = new StringTokenizer(safeExamBrowserKeys);
          tokenizer.hasMoreTokens() && !safe; ) {
        String safeExamBrowserKey = tokenizer.nextToken();
        String hash = Encoder.sha256Exam(url + safeExamBrowserKey);
        if (safeExamHash != null && safeExamHash.equals(hash)) {
          safe = true;
        }
        if (debug) {
          log.debug(
              (safeExamHash.equals(hash) ? "Success" : "Failed")
                  + " : "
                  + safeExamHash
                  + " (Header) "
                  + hash
                  + " (Calculated)");
        }
      }
    } else {
      safe = true;
    }
    return safe;
  }
}
Example #17
0
/**
 * Initial date: 04.11.2014<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
@Service
public class LinkedInProvider implements OAuthSPI {

  private static final OLog log = Tracing.createLoggerFor(LinkedInProvider.class);

  @Autowired private OAuthLoginModule oauthModule;

  @Override
  public boolean isEnabled() {
    return oauthModule.isLinkedInEnabled();
  }

  @Override
  public boolean isRootEnabled() {
    return false;
  }

  @Override
  public boolean isImplicitWorkflow() {
    return false;
  }

  @Override
  public String getName() {
    return "linkedin";
  }

  @Override
  public String getProviderName() {
    return "LINKEDIN";
  }

  @Override
  public String getIconCSS() {
    return "o_icon o_icon_provider_linkedin";
  }

  @Override
  public Api getScribeProvider() {
    return new LinkedInApi();
  }

  @Override
  public String getAppKey() {
    return oauthModule.getLinkedInApiKey();
  }

  @Override
  public String getAppSecret() {
    return oauthModule.getLinkedInApiSecret();
  }

  @Override
  public String[] getScopes() {
    return new String[] {"r_basicprofile", "r_emailaddress"};
  }

  @Override
  public OAuthUser getUser(OAuthService service, Token accessToken) {
    OAuthRequest oauthRequest =
        new OAuthRequest(
            Verb.GET,
            "http://api.linkedin.com/v1/people/~:(id,first-name,last-name,email-address)");
    service.signRequest(accessToken, oauthRequest);
    Response oauthResponse = oauthRequest.send();
    String body = oauthResponse.getBody();
    return parseInfos(body);
  }

  public OAuthUser parseInfos(String body) {
    OAuthUser infos = new OAuthUser();
    try {
      DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
      DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
      Document doc = dBuilder.parse(new InputSource(new StringReader(body)));

      NodeList nodes = doc.getElementsByTagName("person");
      for (int i = 0; i < nodes.getLength(); i++) {
        Element element = (Element) nodes.item(i);
        for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) {
          String localName = node.getNodeName();
          if ("first-name".equals(localName)) {
            infos.setFirstName(getCharacterDataFromElement(node));
          } else if ("last-name".equals(localName)) {
            infos.setLastName(getCharacterDataFromElement(node));
          } else if ("email-address".equals(localName)) {
            infos.setEmail(getCharacterDataFromElement(node));
          } else if ("id".equals(localName)) {
            infos.setId(getCharacterDataFromElement(node));
          }
        }
      }
    } catch (ParserConfigurationException | SAXException | IOException e) {
      log.error("", e);
    }
    return infos;
  }

  public String getCharacterDataFromElement(Node parentNode) {
    StringBuilder sb = new StringBuilder();
    for (Node node = parentNode.getFirstChild(); node != null; node = node.getNextSibling()) {
      if (node.getNodeType() == Node.TEXT_NODE) {
        String text = node.getNodeValue();
        if (StringHelper.containsNonWhitespace(text)) {
          sb.append(text);
        }
      }
    }
    return sb.toString();
  }
}
public class WeeklyStatisticDisplayController extends StatisticDisplayController {

  /** the logging object used in this class * */
  private static final OLog log_ = Tracing.createLoggerFor(WeeklyStatisticDisplayController.class);

  private VelocityContainer weeklyStatisticFormVc_;
  private VelocityContainer weeklyStatisticVc_;
  private DateChooserForm form_;

  public WeeklyStatisticDisplayController(
      final UserRequest ureq,
      final WindowControl windowControl,
      final ICourse course,
      final IStatisticManager statisticManager) {
    super(ureq, windowControl, course, statisticManager);
  }

  @Override
  protected Component createInitialComponent(final UserRequest ureq) {
    setVelocityRoot(Util.getPackageVelocityRoot(getClass()));

    weeklyStatisticVc_ = this.createVelocityContainer("weeklystatisticparent");

    weeklyStatisticFormVc_ = this.createVelocityContainer("weeklystatisticform");
    form_ = new DateChooserForm(ureq, getWindowControl(), 8 * 7);
    listenTo(form_);
    weeklyStatisticFormVc_.put("statisticForm", form_.getInitialComponent());
    weeklyStatisticFormVc_.contextPut("statsSince", getStatsSinceStr(ureq));

    weeklyStatisticVc_.put("weeklystatisticform", weeklyStatisticFormVc_);

    final Component parentInitialComponent = super.createInitialComponent(ureq);
    weeklyStatisticVc_.put("statistic", parentInitialComponent);

    return weeklyStatisticVc_;
  }

  @Override
  public void event(final UserRequest ureq, final Controller source, final Event event) {
    if (source == form_ && event == Event.DONE_EVENT) {
      // need to regenerate the statisticResult
      // and now recreate the table controller
      recreateTableController(ureq);
    }
    super.event(ureq, source, event);
  }

  @Override
  protected StatisticResult recalculateStatisticResult(final UserRequest ureq) {
    // recalculate the statistic result based on the from and to dates.
    // do this by going via sql (see WeeklyStatisticManager)
    final IStatisticManager weeklyStatisticManager = getStatisticManager();
    final StatisticResult statisticResult =
        weeklyStatisticManager.generateStatisticResult(
            ureq,
            getCourse(),
            getCourseRepositoryEntryKey(),
            form_.getFromDate(),
            form_.getToDate());
    return statisticResult;
  }
}
/**
 * Description:<br>
 * Calculates it the user has any assessment news for the notification system. Currently this checks
 * for new tests
 *
 * <p>Initial Date: 21-giu-2005 <br>
 *
 * @author Roberto Bagnoli
 */
public class AssessmentNotificationsHandler implements NotificationsHandler {
  private static final OLog log = Tracing.createLoggerFor(AssessmentNotificationsHandler.class);

  private static final String CSS_CLASS_USER_ICON = "o_icon_user";
  private static AssessmentNotificationsHandler INSTANCE;
  private Map<Long, SubscriptionContext> subsContexts = new HashMap<Long, SubscriptionContext>();

  /** Don't use this! Always use the getInstance() method. This is only used once by Spring! */
  public AssessmentNotificationsHandler() {
    INSTANCE = this;
  }

  /** @return the singleton instance */
  public static AssessmentNotificationsHandler getInstance() {
    return INSTANCE;
  }

  /**
   * Returns the <code>SubscriptionContext</code> to use for assessment notification about specified
   * <code>ICourse</code>.<br>
   * <br>
   * <b>PRE CONDITIONS</b>
   *
   * <ul>
   *   <li><code>course != null</code>
   * </ul>
   *
   * If <code>ident == null</code>, the subscription context is (created and) returned without
   * authorization control
   *
   * @param ident the identity, if null, no subscription check will be made
   * @param course
   * @return the subscription context to use or <code>null</code> if the identity associated to the
   *     request is not allowed to be notified
   * @see #canSubscribeForAssessmentNotification(Identity, ICourse)
   */
  protected SubscriptionContext getAssessmentSubscriptionContext(Identity ident, ICourse course) {
    SubscriptionContext sctx = null;

    if (ident == null || canSubscribeForAssessmentNotification(ident, course)) {
      // Creates a new SubscriptionContext only if not found into cache
      Long courseId = course.getResourceableId();
      synchronized (
          subsContexts) { // o_clusterOK by:ld - no problem to have independent subsContexts caches
                          // for each cluster node
        sctx = subsContexts.get(courseId);
        if (sctx == null) {
          // a subscription context showing to the root node (the course's root
          // node is started when clicking such a notification)
          CourseNode cn = course.getRunStructure().getRootNode();
          CourseEnvironment ce = course.getCourseEnvironment();
          // FIXME:fg:b little problem is that the assessment tool and the course are not "the same"
          // anymore, that is you can open the same course twice in the
          // dynamic tabs by a) klicking e.g. via repo, and b via notifications link to the
          // assementtool
          sctx =
              new SubscriptionContext(
                  CourseModule.ORES_COURSE_ASSESSMENT, ce.getCourseResourceableId(), cn.getIdent());
          subsContexts.put(courseId, sctx);
        }
      }
    }

    return sctx;
  }

  /**
   * Shortcut for <code>getAssessmentSubscriptionContext((Identity) null, course)</code>
   *
   * @param course
   * @return the AssessmentSubscriptionContext
   * @see #getAssessmentSubscriptionContext(Identity, ICourse)
   */
  private SubscriptionContext getAssessmentSubscriptionContext(ICourse course) {
    return getAssessmentSubscriptionContext((Identity) null, course);
  }

  /**
   * Return <code>PublisherData</code> instance to use for assessment notification.<br>
   * <br>
   * <b>PRE CONDITIONS</b>
   *
   * <ul>
   *   <li><code>course != null</code>
   * </ul>
   *
   * @param course
   * @param the business path
   * @return the publisherdata
   */
  public PublisherData getAssessmentPublisherData(ICourse course, String businessPath) {
    return new PublisherData(
        CourseModule.ORES_COURSE_ASSESSMENT,
        String.valueOf(course.getCourseEnvironment().getCourseResourceableId()),
        businessPath);
  }

  /**
   * Signal the <code>NotificationsManager</code> about assessment news available for a course.<br>
   * <br>
   * <b>PRE CONDITIONS</b>
   *
   * <ul>
   *   <li><code>courseId != null</code>
   * </ul>
   *
   * @param courseId the resourceable id of the course to signal news about
   * @param ident the identity to ignore news for
   */
  public void markPublisherNews(Identity ident, Long courseId) {
    ICourse course = loadCourseFromId(courseId);
    if (course == null) throw new AssertException("course with id " + courseId + " not found!");
    markPublisherNews(ident, course);
  }

  /**
   * Signal the <code>NotificationsManager</code> about assessment news available on a course.<br>
   * <br>
   * <b>PRE CONDITIONS</b>
   *
   * <ul>
   *   <li><code>course != null</code>
   * </ul>
   *
   * @param course the course to signal news about
   * @param ident the identity to ignore news for
   */
  private void markPublisherNews(Identity ident, ICourse course) {
    SubscriptionContext subsContext = getAssessmentSubscriptionContext(course);
    if (subsContext != null) {
      NotificationsManager.getInstance().markPublisherNews(subsContext, ident, true);
    }
  }

  /**
   * Assessment notification rights check.<br>
   * Tests if an <code>Identity</code> can subscribe for assessment notification for the specified
   * <code>ICourse</code>.<br>
   * <br>
   * <b>PRE CONDITIONS</b>
   *
   * <ul>
   *   <li><code>course != null</code>
   * </ul>
   *
   * @param ident the identity to check rights for. Can be <code>null</code>
   * @param course the course to check rights against
   * @return if <code>ident == null</code> this method always returns false; otherwise subscriptions
   *     rights are met only by course administrators and course coaches
   */
  private boolean canSubscribeForAssessmentNotification(Identity ident, ICourse course) {
    if (ident == null) return false;

    CourseGroupManager grpMan = course.getCourseEnvironment().getCourseGroupManager();

    boolean isInstitutionalResourceManager =
        BaseSecurityManager.getInstance()
            .isIdentityInSecurityGroup(
                ident,
                BaseSecurityManager.getInstance()
                    .findSecurityGroupByName(Constants.GROUP_INST_ORES_MANAGER));
    return isInstitutionalResourceManager
        || grpMan.isIdentityCourseAdministrator(ident)
        || grpMan.isIdentityCourseCoach(ident)
        || grpMan.hasRight(ident, CourseRights.RIGHT_ASSESSMENT);
  }

  /**
   * Utility method.<br>
   * Load an instance of <code>ICourse</code> given its numeric resourceable id
   */
  private ICourse loadCourseFromId(Long courseId) {
    return CourseFactory.loadCourse(courseId);
  }

  /**
   * Utility method.<br>
   * Build (recursively) the list of all test nodes belonging to the specified <code>ICourse</code>.
   * <br>
   * The returned <code>List</code> is empty if course has no AssessableCourseNode. Structure course
   * node are excluded from the list.<br>
   * <br>
   * <b>PRE CONDITIONS</b>
   *
   * <ul>
   *   <li><code>course != null</code>
   * </ul>
   *
   * <br>
   * <b>POST CONDITIONS</b>
   *
   * <ul>
   *   <li>The returned list, if not empty, contains ONLY instances of type <code>
   *       AssessableCourseNode</code>
   * </ul>
   */
  private List<AssessableCourseNode> getCourseTestNodes(ICourse course) {
    List<AssessableCourseNode> assessableNodes = new ArrayList<AssessableCourseNode>();

    Structure courseStruct = course.getRunStructure();
    CourseNode rootNode = courseStruct.getRootNode();

    getCourseTestNodes(rootNode, assessableNodes);

    return assessableNodes;
  }

  /**
   * Recursive step used by <code>getCourseAssessableNodes(ICourse)</code>.<br>
   * <br>
   * <b>PRE CONDITIONS</b>
   *
   * <ul>
   *   <li><code>course != null</code>
   *   <li><code>result != null</code>
   * </ul>
   *
   * @see #getCourseTestNodes(ICourse)
   */
  private void getCourseTestNodes(INode node, List<AssessableCourseNode> result) {
    if (node != null) {
      if (node instanceof AssessableCourseNode && !(node instanceof STCourseNode)) {
        result.add((AssessableCourseNode) node);
      }

      for (int i = 0; i < node.getChildCount(); i++) {
        getCourseTestNodes(node.getChildAt(i), result);
      }
    }
  }

  /**
   * @see
   *     org.olat.core.commons.services.notifications.NotificationsHandler#createSubscriptionInfo(org.olat.core.commons.services.notifications.Subscriber,
   *     java.util.Locale, java.util.Date)
   */
  public SubscriptionInfo createSubscriptionInfo(
      final Subscriber subscriber, Locale locale, Date compareDate) {
    SubscriptionInfo si = null;
    Publisher p = subscriber.getPublisher();
    if (!NotificationsUpgradeHelper.checkCourse(p)) {
      // course don't exist anymore
      NotificationsManager.getInstance().deactivate(p);
      return NotificationsManager.getInstance().getNoSubscriptionInfo();
    }

    try {
      Date latestNews = p.getLatestNewsDate();
      Identity identity = subscriber.getIdentity();

      // do not try to create a subscription info if state is deleted - results in
      // exceptions, course
      // can't be loaded when already deleted
      if (NotificationsManager.getInstance().isPublisherValid(p)
          && compareDate.before(latestNews)) {
        Long courseId = new Long(p.getData());
        final ICourse course = loadCourseFromId(courseId);
        if (course != null) {
          // course admins or users with the course right to have full access to
          // the assessment tool will have full access to user tests
          CourseGroupManager cgm = course.getCourseEnvironment().getCourseGroupManager();
          final boolean hasFullAccess =
              (cgm.isIdentityCourseAdministrator(identity)
                  ? true
                  : cgm.hasRight(identity, CourseRights.RIGHT_ASSESSMENT));
          final List<Identity> coachedUsers = new ArrayList<Identity>();
          if (!hasFullAccess) {
            // initialize list of users, only when user has not full access
            List<BusinessGroup> coachedGroups = cgm.getOwnedBusinessGroups(identity);
            BusinessGroupService businessGroupService =
                CoreSpringFactory.getImpl(BusinessGroupService.class);
            List<Identity> coachedIdentites =
                businessGroupService.getMembers(coachedGroups, GroupRoles.participant.name());
            coachedUsers.addAll(coachedIdentites);
          }

          List<AssessableCourseNode> testNodes = getCourseTestNodes(course);
          Translator translator =
              Util.createPackageTranslator(AssessmentNotificationsHandler.class, locale);

          for (AssessableCourseNode test : testNodes) {
            final CoursePropertyManager cpm =
                course.getCourseEnvironment().getCoursePropertyManager();

            List<Property> scoreProperties =
                cpm.listCourseNodeProperties(test, null, null, AssessmentManager.SCORE);
            List<Property> attemptProperties =
                cpm.listCourseNodeProperties(test, null, null, AssessmentManager.ATTEMPTS);

            for (Property attemptProperty : attemptProperties) {
              Date modDate = attemptProperty.getLastModified();
              Identity assessedIdentity = attemptProperty.getIdentity();
              if (modDate.after(compareDate)
                  && (hasFullAccess
                      || PersistenceHelper.listContainsObjectByKey(
                          coachedUsers, assessedIdentity))) {
                String score = null;
                for (Property scoreProperty : scoreProperties) {
                  if (scoreProperty.getIdentity().equalsByPersistableKey(assessedIdentity)) {
                    score = scoreProperty.getFloatValue().toString();
                    break;
                  }
                }

                if (test instanceof ScormCourseNode) {
                  ScormCourseNode scormTest = (ScormCourseNode) test;
                  // check if completed or passed
                  String status =
                      ScormAssessmentManager.getInstance()
                          .getLastLessonStatus(
                              assessedIdentity.getName(), course.getCourseEnvironment(), scormTest);
                  if (!"passed".equals(status) && !"completed".equals(status)) {
                    continue;
                  }
                }

                String desc;
                String type = translator.translate("notifications.entry." + test.getType());
                if (score == null) {
                  desc =
                      translator.translate(
                          "notifications.entry.attempt",
                          new String[] {
                            test.getShortTitle(),
                            NotificationHelper.getFormatedName(assessedIdentity),
                            type
                          });
                } else {
                  desc =
                      translator.translate(
                          "notifications.entry",
                          new String[] {
                            test.getShortTitle(),
                            NotificationHelper.getFormatedName(assessedIdentity),
                            score,
                            type
                          });
                }

                String urlToSend = null;
                String businessPath = null;
                if (p.getBusinessPath() != null) {
                  businessPath =
                      p.getBusinessPath()
                          + "[assessmentTool:0][Identity:"
                          + assessedIdentity.getKey()
                          + "][CourseNode:"
                          + test.getIdent()
                          + "]";
                  urlToSend =
                      BusinessControlFactory.getInstance()
                          .getURLFromBusinessPathString(businessPath);
                }

                SubscriptionListItem subListItem =
                    new SubscriptionListItem(
                        desc, urlToSend, businessPath, modDate, CSS_CLASS_USER_ICON);
                if (si == null) {
                  String title =
                      translator.translate(
                          "notifications.header", new String[] {course.getCourseTitle()});
                  String css =
                      CourseNodeFactory.getInstance()
                          .getCourseNodeConfigurationEvenForDisabledBB(test.getType())
                          .getIconCSSClass();
                  si =
                      new SubscriptionInfo(
                          subscriber.getKey(), p.getType(), new TitleItem(title, css), null);
                }
                si.addSubscriptionListItem(subListItem);
              }
            }
          }
        }
      }
      if (si == null) {
        si = NotificationsManager.getInstance().getNoSubscriptionInfo();
      }
      return si;
    } catch (Exception e) {
      log.error("Error while creating assessment notifications", e);
      checkPublisher(p);
      return NotificationsManager.getInstance().getNoSubscriptionInfo();
    }
  }

  private void checkPublisher(Publisher p) {
    try {
      if (!NotificationsUpgradeHelper.checkCourse(p)) {
        log.info("deactivating publisher with key; " + p.getKey(), null);
        NotificationsManager.getInstance().deactivate(p);
      }
    } catch (Exception e) {
      log.error("", e);
    }
  }

  @Override
  public String createTitleInfo(Subscriber subscriber, Locale locale) {
    try {
      Long resId = subscriber.getPublisher().getResId();
      String displayName =
          RepositoryManager.getInstance().lookupDisplayNameByOLATResourceableId(resId);
      Translator trans = Util.createPackageTranslator(AssessmentNotificationsHandler.class, locale);
      String title = trans.translate("notifications.title", new String[] {displayName});
      return title;
    } catch (Exception e) {
      log.error(
          "Error while creating assessment notifications for subscriber: " + subscriber.getKey(),
          e);
      checkPublisher(subscriber.getPublisher());
      return "-";
    }
  }

  @Override
  public String getType() {
    return "AssessmentManager";
  }
}
Example #20
0
/**
 * Description:<br>
 * Testing i18n Translation Tool and Translation Developers Tools
 *
 * <p>Initial Date: 01.09.2008 <br>
 *
 * @author gnaegi
 * @author rhaag
 */
public class I18nTest extends OlatTestCase {

  private OLog log = Tracing.createLoggerFor(I18nTest.class);

  @Autowired private I18nManager i18nMgr;
  @Autowired private TranslationDevManager tDMgr;
  private static final String testSourceBundle =
      "org.olat.core.util.i18n.junittestdata.devtools.source";
  private static final String testTargetBundle =
      "org.olat.core.util.i18n.junittestdata.devtools.target";
  private static final String testMoveTargetBundle =
      "org.olat.core.util.i18n.junittestdata.movetarget.source";

  /** @see junit.framework.TestCase#setUp() */
  @Before
  public void setup() throws Exception {
    // see prepareDevToolTests() !
    Settings.setJUnitTest(true);
  }

  @After
  public void tearDown() throws Exception {
    String testNewBundle = "org.olat.core.util.i18n.junittestdata.new";
    Locale testLocale = i18nMgr.getLocaleOrDefault("de");
    File baseDir = I18nModule.getPropertyFilesBaseDir(testLocale, testNewBundle);
    // only delete files when basedir available
    if (baseDir == null) return;
    File testFile = i18nMgr.getPropertiesFile(testLocale, testNewBundle, baseDir);
    // delete not only new/_i18n/LocalStrings_de.properties, delete also _i18n and new itself
    if (testFile.getParentFile().getParentFile().exists()) {
      FileUtils.deleteDirsAndFiles(testFile.getParentFile().getParentFile(), true, true);
    }
    // don't do inline translation markup
    i18nMgr.setMarkLocalizedStringsEnabled(null, false);
    // cleanup dev tools test case files
    removeDevToolTests();
  }

  protected void prepareDevToolTests() {
    // create test fixure in devtools.source
    // Roman: you can't check in a 'default' package (illegal package name in java)
    Locale testLocale = i18nMgr.getLocaleOrDefault("de");
    // key 1
    String key = "key.to.move";
    I18nItem i18nItem = i18nMgr.getI18nItem(testSourceBundle, key, testLocale);
    i18nMgr.saveOrUpdateI18nItem(i18nItem, "I have to go");
    i18nMgr.setAnnotation(i18nItem, "an annotation");
    i18nMgr.setKeyPriority(testSourceBundle, key, 100);
    // key 2
    key = "key.to.stay2";
    i18nItem = i18nMgr.getI18nItem(testSourceBundle, key, testLocale);
    i18nMgr.saveOrUpdateI18nItem(i18nItem, "hello $:key.to.move");
  }

  protected void removeDevToolTests() {
    Locale testLocale = i18nMgr.getLocaleOrDefault("de");
    // cleanup devtools source/target files
    // 1) source files
    File baseDir = I18nModule.getPropertyFilesBaseDir(testLocale, testSourceBundle);
    File testSFile = i18nMgr.getPropertiesFile(testLocale, testSourceBundle, baseDir);
    File sourcePath = testSFile.getParentFile().getParentFile();
    FileUtils.deleteDirsAndFiles(sourcePath, true, true);
    // 2) target files
    baseDir = I18nModule.getPropertyFilesBaseDir(testLocale, testTargetBundle);
    File testTFile = i18nMgr.getPropertiesFile(testLocale, testTargetBundle, baseDir);
    File targetPath = testTFile.getParentFile().getParentFile();
    FileUtils.deleteDirsAndFiles(targetPath, true, true);
    // 3) move target files
    baseDir = I18nModule.getPropertyFilesBaseDir(testLocale, testMoveTargetBundle);
    File testMFile = i18nMgr.getPropertiesFile(testLocale, testMoveTargetBundle, baseDir);
    File movePath = testMFile.getParentFile().getParentFile().getParentFile();
    FileUtils.deleteDirsAndFiles(movePath, true, true);
  }

  @Test
  public void testRemoveDeletedKeysTest() {
    // set languages that is used as reference: all keys there are the keys should not to be deleted
    String[] referenceLanguages = new String[] {"de", "en"};
    // set the languages that should be cleaned up
    Set<String> targetLanguages = I18nModule.getTranslatableLanguageKeys();
    // Set<String> targetLanguages = new HashSet<String>();
    // targetLanguages.add("en");
    tDMgr.removeDeletedKeys(false, referenceLanguages, targetLanguages);
  }

  @Test
  public void testMoveKeyTask() {
    if (I18nModule.isTransToolEnabled()) {
      prepareDevToolTests();
      Locale testLocale = i18nMgr.getLocaleOrDefault("de");
      String ktm = "key.to.move";
      i18nMgr.setCachingEnabled(true);
      Properties sourcePropResolved = i18nMgr.getResolvedProperties(testLocale, testSourceBundle);
      Properties sourceProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(testLocale, testSourceBundle);
      String matchString = sourceProp.getProperty(ktm);
      assertTrue(sourcePropResolved.getProperty("key.to.stay2").indexOf(matchString) != -1);

      tDMgr.moveKeyToOtherBundle(testSourceBundle, testTargetBundle, ktm);
      sourceProp = i18nMgr.getPropertiesWithoutResolvingRecursively(testLocale, testSourceBundle);
      Properties targetProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(testLocale, testTargetBundle);
      assertNull(sourceProp.getProperty(ktm));
      assertNotNull(targetProp.getProperty(ktm));
      Properties sourceMetaProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(null, testSourceBundle);
      Properties targetMetaProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(null, testTargetBundle);
      assertNull(sourceMetaProp.getProperty(ktm + I18nManager.METADATA_ANNOTATION_POSTFIX));
      assertNull(sourceMetaProp.getProperty(ktm + I18nManager.METADATA_KEY_PRIORITY_POSTFIX));
      assertTrue(targetMetaProp.containsKey(ktm + I18nManager.METADATA_ANNOTATION_POSTFIX));
      assertEquals(
          "100", targetMetaProp.getProperty(ktm + I18nManager.METADATA_KEY_PRIORITY_POSTFIX));
      // check for changed references in value
      // if correctly done, should still be resolvable
      assertTrue(sourcePropResolved.getProperty("key.to.stay2").indexOf(matchString) != -1);
      tDMgr.logToFile("moveKey");
    }
  }

  @Test
  public void testMovePackageTask() {
    if (I18nModule.isTransToolEnabled()) {
      prepareDevToolTests();
      tDMgr.movePackageTask(testSourceBundle, testTargetBundle);
      i18nMgr.clearCaches();
      Properties sourceProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(null, testSourceBundle);
      assertTrue(sourceProp.isEmpty());
      Properties targetProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(null, testTargetBundle);
      assertFalse(targetProp.isEmpty());
      tDMgr.logToFile("movePackage");
    }
  }

  @Test
  public void testMovePackageByMovingSingleKeysTask() {
    if (I18nModule.isTransToolEnabled()) {
      prepareDevToolTests();
      tDMgr.movePackageByMovingSingleKeysTask(testSourceBundle, testTargetBundle);
      i18nMgr.clearCaches();
      Properties sourceProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(null, testSourceBundle);
      assertTrue(sourceProp.isEmpty());
      Properties targetProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(null, testTargetBundle);
      assertFalse(targetProp.isEmpty());
      tDMgr.logToFile("movePackageSingle");
    }
  }

  @Test
  public void testMergePackageTask() {
    if (I18nModule.isTransToolEnabled()) {
      prepareDevToolTests();
      tDMgr.mergePackageTask(testSourceBundle, testTargetBundle);
      i18nMgr.clearCaches();
      assertTrue(
          i18nMgr.getPropertiesWithoutResolvingRecursively(null, testSourceBundle).isEmpty());
      // TODO: manipulate source/target first, merge and check if all is in target!
      // try to merge existing key
    }
  }

  @Test
  public void testRenameLanguageTask() {
    if (I18nModule.isTransToolEnabled()) {
      prepareDevToolTests();
      // create source
      Locale xxLocale = new Locale("xx");
      String key = "key.to.move";
      I18nItem i18nItem = i18nMgr.getI18nItem(testSourceBundle, key, xxLocale);
      i18nMgr.saveOrUpdateI18nItem(i18nItem, "I have to go");
      i18nMgr.setAnnotation(i18nItem, "an annotation");
      i18nMgr.setKeyPriority(testSourceBundle, key, 100);
      // copy to target
      Locale yyLocale = new Locale("yy");
      tDMgr.renameLanguageTask(xxLocale, yyLocale);
      i18nMgr.clearCaches();
      // test
      Properties deletedProperties =
          i18nMgr.getPropertiesWithoutResolvingRecursively(xxLocale, testSourceBundle);
      assertTrue(deletedProperties.isEmpty());
      Properties targetProp =
          i18nMgr.getPropertiesWithoutResolvingRecursively(yyLocale, testSourceBundle);
      assertFalse(targetProp.isEmpty());
    }
  }

  // TODO: RH: remove this or really test for correct finding?!
  @Test
  public void testGetDouplicateKeys() {
    tDMgr.getDouplicateKeys();
  }

  // TODO: RH: remove this or really test for correct finding?!
  @Test
  public void testGetDouplicateValues() {
    tDMgr.getDouplicateValues();
  }

  @Test
  public void testMoveLanguageTask() {
    if (I18nModule.isTransToolEnabled()) {
      prepareDevToolTests();
      String srcPath =
          I18nModule.getTransToolApplicationLanguagesSrcDir().getAbsolutePath()
              + "/../../test/java/org/olat/core/util/i18n/junittestdata/devtools";
      String targetPath = srcPath + "/../movetarget";
      // only copy: target should exist
      Locale mvLocale = i18nMgr.getLocaleOrDefault("de");
      // test before move
      Properties sourceProperties =
          i18nMgr.getPropertiesWithoutResolvingRecursively(mvLocale, testSourceBundle);
      assertFalse(sourceProperties.isEmpty());
      Properties moveProperties =
          i18nMgr.getPropertiesWithoutResolvingRecursively(mvLocale, testMoveTargetBundle);
      assertTrue(moveProperties.isEmpty());
      // copy
      tDMgr.moveLanguageTask(mvLocale, srcPath, targetPath, false);
      // test after copy
      sourceProperties =
          i18nMgr.getPropertiesWithoutResolvingRecursively(mvLocale, testSourceBundle);
      assertFalse(sourceProperties.isEmpty());
      moveProperties =
          i18nMgr.getPropertiesWithoutResolvingRecursively(mvLocale, testMoveTargetBundle);
      assertFalse(moveProperties.isEmpty());
      // move
      tDMgr.moveLanguageTask(mvLocale, srcPath, targetPath, true);
      // test after move
      sourceProperties =
          i18nMgr.getPropertiesWithoutResolvingRecursively(mvLocale, testSourceBundle);
      assertTrue(sourceProperties.isEmpty());
      moveProperties =
          i18nMgr.getPropertiesWithoutResolvingRecursively(mvLocale, testMoveTargetBundle);
      assertFalse(moveProperties.isEmpty());
    }
  }

  // remove after execution!
  @Test
  public void testRemoveXKeyTask() {
    tDMgr.removeXKeysTask(false);
    tDMgr.logToFile("XKeys");
  }

  // remove after execution!
  @Test
  public void testRemoveTodoKeyTask() {
    tDMgr.removeTodoKeysTask(false);
    tDMgr.logToFile("todoKeys");
  }

  // remove after execution!
  @Test
  public void testRemoveEmptyKeysTask() {
    tDMgr.removeEmptyKeysTask(false);
    tDMgr.logToFile("emptyKeys");
  }

  // remove after execution!
  @Test
  public void testRemoveReferenceLanguageCopiesTask() {
    if (I18nModule.isTransToolEnabled()) {
      tDMgr.removeReferenceLanguageCopiesTask(false);
      tDMgr.logToFile("refLangCopied");
    }
  }

  /** Test method i18nManager.searchForAvailableLanguages() */
  @Test
  public void testSearchForAvailableLanguages() {
    if (I18nModule.isTransToolEnabled()) {
      // Try to load i18n files and a jar from the testdata dir
      File testDataDir =
          new File(
              I18nModule.getTransToolApplicationLanguagesSrcDir(),
              "/../../test/java/org/olat/core/util/i18n/junittestdata/");
      assertTrue(testDataDir.exists());
      Set<String> foundLanguages = i18nMgr.searchForAvailableLanguages(testDataDir);
      // Set must contain some LocalStrings file:
      assertTrue(foundLanguages.contains("de"));
      assertTrue(foundLanguages.contains("en"));
      assertTrue(foundLanguages.contains("pt_BR"));
      // Set must contain some LocaleStrings from the jar package
      assertTrue(foundLanguages.contains("fr"));
      assertTrue(foundLanguages.contains("zh_CN"));
      // Final check
      assertEquals(6, foundLanguages.size());
    } else {
      Set<String> foundLanguages = I18nModule.getAvailableLanguageKeys();
      // Set must contain some LocaleStrings from the jar package
      assertTrue(foundLanguages.contains("fr"));
      assertTrue(foundLanguages.contains("zh_CN"));
    }
  }

  /** Test method i18nManager.searchForBundleNamesContainingI18nFiles() */
  @Test
  public void testSearchForBundleNamesContainingI18nFiles() {
    long start = System.currentTimeMillis();
    List<String> foundBundles = i18nMgr.searchForBundleNamesContainingI18nFiles();
    long end = System.currentTimeMillis();
    log.info(
        "Searching for "
            + foundBundles.size()
            + " bundles on OLAT source path took me "
            + (end - start)
            + "ms",
        "testSearchForBundleNamesContainingI18nFiles");
    // Must contain packages from core
    assertTrue(foundBundles.contains("org.olat.core"));
    assertTrue(foundBundles.contains("org.olat.core.gui.control"));
    assertTrue(foundBundles.contains("org.olat.core.commons.fullWebApp"));
    // Must contain packages from olat app
    assertTrue(foundBundles.contains("org.olat"));
    assertTrue(foundBundles.contains("org.olat.course"));
  }

  /** Test method i18nManager.buildI18nFilename() */
  @Test
  public void testBuildI18nFilename() {
    String overlay = I18nModule.getOverlayName();
    String testFileName = i18nMgr.buildI18nFilename(new Locale("de"));
    assertEquals("LocalStrings_de.properties", testFileName);
    testFileName = i18nMgr.buildI18nFilename(new Locale("de", "", "__" + overlay));
    assertEquals("LocalStrings_de__" + overlay + ".properties", testFileName);
    testFileName = i18nMgr.buildI18nFilename(new Locale("de", "CH"));
    assertEquals("LocalStrings_de_CH.properties", testFileName);
    testFileName = i18nMgr.buildI18nFilename(new Locale("de", "CH", "VENDOR"));
    assertEquals("LocalStrings_de_CH_VENDOR.properties", testFileName);
    testFileName = i18nMgr.buildI18nFilename(new Locale("de", "CH", "VENDOR__" + overlay));
    assertEquals("LocalStrings_de_CH_VENDOR__" + overlay + ".properties", testFileName);
  }

  /** Test methods i18nManager.getLocaleOrFallback() */
  @Test
  public void testGetLocaleOrFallback() {
    // Normal case
    Locale locale = i18nMgr.getLocaleOrDefault("de");
    assertNotNull(locale);
    assertEquals(i18nMgr.getLocaleOrNull("de"), locale);
    // Unexisting locale case
    locale = i18nMgr.getLocaleOrDefault("xy");
    assertEquals(I18nModule.getDefaultLocale(), locale);
    // Test trying to get overlay via getLocale method which should not return the overlay
    Locale overlay = i18nMgr.getLocaleOrDefault("de__" + I18nModule.getOverlayName());
    assertEquals(I18nModule.getDefaultLocale(), overlay);
    overlay = i18nMgr.getLocaleOrDefault("zh_CN__" + I18nModule.getOverlayName());
    assertEquals(I18nModule.getDefaultLocale(), overlay);
  }

  /** Test methods i18nManager.createLocal() */
  @Test
  public void testCreateLocale() {
    // standard locale
    Locale loc = i18nMgr.createLocale("de");
    assertNotNull(loc);
    assertEquals("de", loc.getLanguage());
    assertEquals("", loc.getCountry());
    assertEquals("", loc.getVariant());
    // with country
    loc = i18nMgr.createLocale("de_CH");
    assertNotNull(loc);
    assertEquals("de", loc.getLanguage());
    assertEquals("CH", loc.getCountry());
    assertEquals("", loc.getVariant());
    // with variant
    loc = i18nMgr.createLocale("de_CH_ZH");
    assertNotNull(loc);
    assertEquals("de", loc.getLanguage());
    assertEquals("CH", loc.getCountry());
    assertEquals("ZH", loc.getVariant());
    // with variant but no country
    loc = i18nMgr.createLocale("de__VENDOR");
    assertNotNull(loc);
    assertEquals("de", loc.getLanguage());
    assertEquals("", loc.getCountry());
    assertEquals("VENDOR", loc.getVariant());
    //
    // With overlay
    // with language
    String overlay = I18nModule.getOverlayName();
    loc = i18nMgr.createLocale("de");
    Locale over = i18nMgr.createOverlay(loc);
    assertEquals("de____" + overlay, over.toString());
    assertEquals(i18nMgr.createOverlayKeyForLanguage(loc.toString()), i18nMgr.getLocaleKey(over));
    // with country
    loc = i18nMgr.createLocale("de_CH");
    over = i18nMgr.createOverlay(loc);
    assertEquals("de_CH___" + overlay, over.toString());
    assertEquals(i18nMgr.createOverlayKeyForLanguage(loc.toString()), i18nMgr.getLocaleKey(over));
    // with variant
    loc = i18nMgr.createLocale("de_CH_ZH");
    over = i18nMgr.createOverlay(loc);
    assertEquals("de_CH_ZH__" + overlay, over.toString());
    assertEquals(i18nMgr.createOverlayKeyForLanguage(loc.toString()), i18nMgr.getLocaleKey(over));
  }

  /** Test methods i18nManager.getLanguageTranslated() i18nManager.getLanguageInEnglish() */
  @Test
  public void testGetLanguageIn() {
    assertEquals("German", i18nMgr.getLanguageInEnglish("de", false));
    assertEquals("Deutsch", i18nMgr.getLanguageTranslated("de", false));
    assertEquals("English", i18nMgr.getLanguageInEnglish("en", false));
    assertEquals("English", i18nMgr.getLanguageTranslated("en", false));
  }

  /** Test methods i18nManager.getLanguageTranslated() i18nManager.getLanguageInEnglish() */
  @Test
  public void testGetPropertyFile() {
    if (I18nModule.isTransToolEnabled()) {
      File baseDir =
          I18nModule.getPropertyFilesBaseDir(i18nMgr.getLocaleOrDefault("de"), "org.olat.core");
      assertNotNull(baseDir);
      File file =
          i18nMgr.getPropertiesFile(i18nMgr.getLocaleOrDefault("de"), "org.olat.core", baseDir);
      assertTrue(file.exists());
      // test get metadata file
      file = i18nMgr.getPropertiesFile(null, "org.olat.core", baseDir);
      assertNotNull(file);
      try {
        file = i18nMgr.getPropertiesFile(i18nMgr.getLocaleOrDefault("de"), null, baseDir);
        fail("exception expected for NULL bundle");
      } catch (AssertException e) {
        // do nothing, expected
      }
      // test with file that does not exist
      file = i18nMgr.getPropertiesFile(i18nMgr.getLocaleOrDefault("de"), "hello.world", baseDir);
      assertFalse(file.exists());
    }
  }

  /**
   * Test methods i18nManager.getProperties(), i18nManager.saveOrUpdateProperties() and
   * i18nManager.deleteProperties()
   */
  @Ignore
  @Test
  public void testGetSaveOrUpdateAndDeleteProperties() {
    // test with existing files
    Properties props =
        i18nMgr.getResolvedProperties(i18nMgr.getLocaleOrDefault("de"), "org.olat.core");
    assertNotNull(props);
    assertTrue(props.size() > 0);
    // test with non existing files
    String testNewBundle = "org.olat.core.util.i18n.junittestdata.new";
    Locale testLocale = i18nMgr.getLocaleOrDefault("de");
    File baseDir = I18nModule.getPropertyFilesBaseDir(testLocale, testNewBundle);
    File testFile = i18nMgr.getPropertiesFile(testLocale, testNewBundle, baseDir);
    // clean first existing files from previous broken testcase
    if (testFile.exists()) {
      i18nMgr.deleteProperties(testLocale, testNewBundle);
    }
    props = i18nMgr.getResolvedProperties(testLocale, testNewBundle);
    assertNotNull(props);
    assertEquals(0, props.size());
    // test deleting of non existing
    assertFalse(testFile.exists());
    i18nMgr.deleteProperties(testLocale, testNewBundle);
    assertFalse(testFile.exists());
    // test saving of non existing
    props.setProperty("test.key", "Hello wörld !");
    i18nMgr.saveOrUpdateProperties(props, testLocale, testNewBundle);
    assertTrue(testFile.exists());
    assertEquals(
        "Hello wörld !",
        i18nMgr.getLocalizedString(testNewBundle, "test.key", null, testLocale, false, false));
    // test saving of existing
    props = new Properties();
    props.setProperty("hello.world", "&%çest françias, ê alors");
    i18nMgr.saveOrUpdateProperties(props, testLocale, testNewBundle);
    assertTrue(testFile.exists());
    assertEquals(
        "&%çest françias, ê alors",
        i18nMgr.getLocalizedString(testNewBundle, "hello.world", null, testLocale, false, false));
    assertFalse(
        "Hello wörld !"
            .equals(
                i18nMgr.getLocalizedString(
                    testNewBundle, "test.key", null, testLocale, false, false)));
    // test various special cases
    props.setProperty("chinesetest", "请选择你的大学");
    props.setProperty("specialtest", "that's like \"really\" bad");
    props.setProperty("multiline", "bla\tbla\nsecond line");
    props.setProperty("html", "<h1>Hello World</h1>");
    props.setProperty("empty.property", "");
    i18nMgr.saveOrUpdateProperties(props, testLocale, testNewBundle);
    i18nMgr.clearCaches();
    assertEquals(
        "请选择你的大学",
        i18nMgr.getLocalizedString(testNewBundle, "chinesetest", null, testLocale, false, false));
    assertEquals(
        "that's like \"really\" bad",
        i18nMgr.getLocalizedString(testNewBundle, "specialtest", null, testLocale, false, false));
    assertEquals(
        "bla\tbla\nsecond line",
        i18nMgr.getLocalizedString(testNewBundle, "multiline", null, testLocale, false, false));
    assertEquals(
        "<h1>Hello World</h1>",
        i18nMgr.getLocalizedString(testNewBundle, "html", null, testLocale, false, false));
    assertEquals(
        "",
        i18nMgr.getLocalizedString(
            testNewBundle, "empty.property", null, testLocale, false, false));
    assertEquals(
        null,
        i18nMgr.getLocalizedString(
            testNewBundle, "not.existing.property", null, testLocale, false, false));
    // test deleting of existing
    i18nMgr.deleteProperties(testLocale, testNewBundle);
    assertFalse(testFile.exists());
    assertFalse(
        "Hello wörld !"
            .equals(
                i18nMgr.getLocalizedString(
                    testNewBundle, "test.key", null, testLocale, false, false)));
    assertFalse(
        "&%çest françias, ê alors"
            .equals(
                i18nMgr.getLocalizedString(
                    testNewBundle, "hello.world", null, testLocale, false, false)));
    // clean up
    if (testFile.exists()) {
      i18nMgr.deleteProperties(testLocale, testNewBundle);
    }
    if (testFile.getParentFile().exists()) testFile.getParentFile().delete();
  }

  /** Test methods i18nManager.getBundlePriority(), i18nManager.getKeyPriority() */
  @Test
  public void testGetBundleAndKeyPriority() {
    // existing bundle prios
    String bundleName = "org.olat.core.util.i18n.junittestdata";
    Properties metadataProperties =
        i18nMgr.getPropertiesWithoutResolvingRecursively(null, bundleName);
    assertNotNull(metadataProperties);
    assertTrue(metadataProperties.size() > 0);
    assertEquals(900, i18nMgr.getBundlePriority(bundleName));
    assertEquals(
        20, i18nMgr.getKeyPriority(metadataProperties, "no.need.to.translate.this", bundleName));
    // default values
    bundleName = "org.olat.core.util.i18n.junittestdata.subtest";
    metadataProperties = i18nMgr.getPropertiesWithoutResolvingRecursively(null, bundleName);
    assertNotNull(metadataProperties);
    assertTrue(metadataProperties.size() == 0);
    assertEquals(
        I18nManager.DEFAULT_BUNDLE_PRIORITY,
        i18nMgr.getBundlePriority("bla.bla")); // default priority
    assertEquals(
        I18nManager.DEFAULT_KEY_PRIORITY,
        i18nMgr.getKeyPriority(metadataProperties, "no.need.to.translate.this", bundleName));
  }

  /** Test methods i18nManager.getAnnotation() */
  @Test
  public void testGetAnnotation() {
    // existing bundle prios
    String bundleName = "org.olat.core.util.i18n.junittestdata";
    Locale testLocale = i18nMgr.getLocaleOrDefault("de");
    List<I18nItem> items = i18nMgr.findExistingI18nItems(testLocale, bundleName, false);
    for (I18nItem item : items) {
      String annotation = i18nMgr.getAnnotation(item);
      if (item.getKey().equals("no.need.to.translate.this")) {
        assertEquals("this is an annotation", annotation);
      } else {
        assertNull(annotation);
      }
    }
  }

  /** Test methods i18nManager.countI18nItems() and i18nManager.countBundles() */
  @Ignore
  @Test
  public void testCountI18nItemsAndBundles() {
    I18nModule.initBundleNames(); // remove dirty stuff from previous tests
    int bundleCounter = I18nModule.getBundleNamesContainingI18nFiles().size();
    String testNewBundle = "org.olat.core.util.i18n.junittestdata.new";
    Locale testLocale = i18nMgr.getLocaleOrDefault("de");
    File baseDir = I18nModule.getPropertyFilesBaseDir(testLocale, testNewBundle);
    File testFile = i18nMgr.getPropertiesFile(testLocale, testNewBundle, baseDir);
    // clean first existing files from previous broken testcase
    if (testFile.exists()) {
      i18nMgr.deleteProperties(testLocale, testNewBundle);
    }
    Properties props = i18nMgr.getResolvedProperties(testLocale, testNewBundle);
    assertEquals(0, i18nMgr.countI18nItems(testLocale, testNewBundle, true));
    assertEquals(0, i18nMgr.countI18nItems(testLocale, testNewBundle, false));
    props.setProperty("key.1", "1");
    props.setProperty("key.2", "2");
    i18nMgr.saveOrUpdateProperties(props, testLocale, testNewBundle);
    assertEquals(2, i18nMgr.countI18nItems(testLocale, testNewBundle, false));
    assertEquals(0, i18nMgr.countI18nItems(i18nMgr.getLocaleOrDefault("en"), testNewBundle, false));
    assertEquals(bundleCounter + 1, I18nModule.getBundleNamesContainingI18nFiles().size());
    // test all bundles
    int allCount = i18nMgr.countI18nItems(testLocale, null, true);
    assertEquals(allCount, i18nMgr.countI18nItems(testLocale, null, false));
    props.remove("key.1");
    i18nMgr.saveOrUpdateProperties(props, testLocale, testNewBundle);
    assertEquals(allCount - 1, i18nMgr.countI18nItems(testLocale, null, false));
    i18nMgr.deleteProperties(testLocale, testNewBundle);
    assertEquals(allCount - 2, i18nMgr.countI18nItems(testLocale, null, false));
    assertEquals(bundleCounter, I18nModule.getBundleNamesContainingI18nFiles().size());
    // count bundles tests
    assertEquals(0, i18nMgr.countBundles("org.olat.core.util.i18n.nonexisting", true));
    assertEquals(1, i18nMgr.countBundles("org.olat.core.util.i18n.ui", true));
    // finds 4: regular: i18n.devtools, i18n.ui;
    assertEquals(2, i18nMgr.countBundles("org.olat.core.util.i18n", true));
    assertEquals(0, i18nMgr.countBundles("org.olat.core.util.i18n", false));
    assertEquals(1, i18nMgr.countBundles("org.olat.core.util.i18n.ui", false));
    assertTrue(0 < i18nMgr.countBundles(null, false));
    // clean up
    if (testFile.exists()) {
      i18nMgr.deleteProperties(testLocale, testNewBundle);
    }
    if (testFile.getParentFile().exists()) {
      testFile.getParentFile().delete();
    }
  }

  /** Test methods i18nManager.findInKeys() i18nManager.findInValues() */
  @Test
  public void testFindInKeysAndFindInValues() {
    Locale testLocale = i18nMgr.getLocaleOrDefault("de");
    // in keys speedtest
    long start = System.currentTimeMillis();
    List<I18nItem> foundTransItems =
        i18nMgr.findI18nItemsByKeySearch("menu", testLocale, testLocale, null, true);
    long end = System.currentTimeMillis();
    log.info(
        "Searching for term 'menu' took me "
            + (end - start)
            + "ms, found "
            + foundTransItems.size()
            + " item",
        "testFindInKeys");
    // in values speedtest
    start = System.currentTimeMillis();
    foundTransItems =
        i18nMgr.findI18nItemsByValueSearch("OLAT", testLocale, testLocale, null, true);
    int uppercaseSize = foundTransItems.size();
    end = System.currentTimeMillis();
    log.info(
        "Searching for term 'OLAT' took me "
            + (end - start)
            + "ms, found "
            + foundTransItems.size()
            + " item",
        "testFindInKeys");
    // uppercase and lowercase must find the same values
    foundTransItems =
        i18nMgr.findI18nItemsByValueSearch("olat", testLocale, testLocale, null, true);
    assertEquals(uppercaseSize, foundTransItems.size());
    // find in values and regexp tests: * is replaced with regexp,
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch(
                "Eingabefehler aufgetreten", testLocale, testLocale, null, true)
            .size());
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch(
                "Eingabefehler*aufgetreten", testLocale, testLocale, null, true)
            .size());
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch(
                "Eingabefe*ler*aufgetreten", testLocale, testLocale, null, true)
            .size());
    assertEquals(
        2,
        i18nMgr
            .findI18nItemsByValueSearch(
                "Bitte f*llen Sie dieses Feld aus", testLocale, testLocale, null, true)
            .size());
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch(
                "Bitte f*llen Sie dieses Feld aus", testLocale, testLocale, "org.olat.core", false)
            .size());
    // escape reserved regexp keywords
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch("OpenOLAT", testLocale, testLocale, "org.olat.core", false)
            .size());
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch("nOLAT", testLocale, testLocale, "org.olat.core", false)
            .size());
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch("*learning", testLocale, testLocale, "org.olat.core", false)
            .size());
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch(
                "Eingaben</i> </br >*<b>bold</b>*<br>_<i>italic</i>_<br>* Listen",
                testLocale,
                testLocale,
                null,
                true)
            .size());
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch(
                "dargestellt werden. Bitte rufen Sie", testLocale, testLocale, null, true)
            .size());
    // multi line value search
    assertEquals(
        1,
        i18nMgr
            .findI18nItemsByValueSearch(
                "Die Aktion konnte nicht ausgef\u00FChrt werden",
                testLocale,
                testLocale,
                null,
                true)
            .size());
  }

  /** Test methods i18nManager.findMissingI18nItems() */
  @Test
  public void testFindMissingI18nItems() {
    Locale sourceLocale = i18nMgr.getLocaleOrDefault("de");
    Locale targetLocale = i18nMgr.getLocaleOrDefault("de");
    String notExistsTestBundle = "org.olat.core.util.i18n.junittestdata.notexists";
    assertEquals(0, i18nMgr.findMissingI18nItems(sourceLocale, targetLocale, null, true).size());
    assertEquals(
        0,
        i18nMgr.findMissingI18nItems(sourceLocale, targetLocale, notExistsTestBundle, true).size());
    assertEquals(
        0,
        i18nMgr
            .findMissingI18nItems(sourceLocale, targetLocale, notExistsTestBundle, false)
            .size());
    targetLocale = new Locale("xy");
    int total = i18nMgr.findExistingI18nItems(sourceLocale, null, true).size();
    assertEquals(
        total, i18nMgr.findMissingI18nItems(sourceLocale, targetLocale, null, true).size());
    assertEquals(
        total,
        i18nMgr.findExistingAndMissingI18nItems(sourceLocale, targetLocale, null, true).size());
  }

  /**
   * Test methods i18nManager inline translation tool and
   * InlineTranslationInterceptHandlerController
   */
  @Test
  public void testInlineTranslationReplaceLocalizationMarkupWithHTML() {
    // enable inline translation markup
    i18nMgr.setMarkLocalizedStringsEnabled(null, true);
    Translator inlineTrans =
        Util.createPackageTranslator(
            InlineTranslationInterceptHandlerController.class, i18nMgr.getLocaleOrNull("de"));
    URLBuilder inlineTranslationURLBuilder = new jUnitURLBuilder();
    String testBundle = "org.olat.core.util.i18n.junittestdata";
    String testKey = "no.need.to.translate.this";
    String rawtext1 = "Lorem impsum<b>nice stuff</b>";
    String rawtext2 = "Holderadio <ul>lsdfsdf<y  asdf blblb";
    String combinedKey = testBundle + ":" + testKey;
    // test method that adds identifyers around the translation items
    String plainVanillaWrapped =
        i18nMgr.getLocalizedString(
            testBundle, testKey, null, i18nMgr.getLocaleOrNull("de"), false, false);
    String plainVanillaPlain = "just a test";
    Pattern plainVanillaWrappedPattern =
        Pattern.compile(
            I18nManager.IDENT_PREFIX
                + combinedKey
                + ":([0-9]*?)"
                + I18nManager.IDENT_START_POSTFIX
                + plainVanillaPlain
                + I18nManager.IDENT_PREFIX
                + combinedKey
                + ":\\1"
                + I18nManager.IDENT_END_POSTFIX);
    Matcher m = plainVanillaWrappedPattern.matcher(plainVanillaWrapped);
    assertTrue(m.find());

    // test find-replace translator identifyers with HTML markup
    StringOutput inlineTransLink = new StringOutput();
    String[] args = (combinedKey + ":1000000001").split(":");
    InlineTranslationInterceptHandlerController.buildInlineTranslationLink(
        args, inlineTransLink, inlineTrans, inlineTranslationURLBuilder);
    // Plain vanilla text
    String convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            plainVanillaWrapped, inlineTranslationURLBuilder, inlineTrans);
    assertEquals(
        "<span class=\"b_translation_i18nitem\">"
            + inlineTransLink.toString()
            + plainVanillaPlain
            + "</span>",
        convertedToHTMLMarkup);
    // Simple link
    String linkOPEN =
        "<a href=\"http://www.olat.org/bla/blu:bli#bla\" title='funny title' class=\"b_css b_anothercss\">";
    String linkCLOSE = "</a>";
    String inlineSpanOPEN = "<span class=\"b_translation_i18nitem\">";
    String inlineSpanCLOSE = "</span>";
    String translatedWithinLink = linkOPEN + plainVanillaWrapped + linkCLOSE + rawtext1;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedWithinLink, inlineTranslationURLBuilder, inlineTrans);
    String convertedWithinLinkExpected =
        inlineSpanOPEN
            + inlineTransLink.toString()
            + linkOPEN
            + plainVanillaPlain
            + linkCLOSE
            + inlineSpanCLOSE
            + rawtext1;
    assertEquals(convertedWithinLinkExpected, convertedToHTMLMarkup);
    // Simple link with span
    String linkSpanOPEN = "<span class=\"bluber\">";
    String linkSpanCLOSE = "</span>";
    String translatedWithinLinkAndSpan =
        rawtext2 + linkOPEN + linkSpanOPEN + plainVanillaWrapped + linkSpanCLOSE + linkCLOSE;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedWithinLinkAndSpan, inlineTranslationURLBuilder, inlineTrans);
    String convertedWithinLinkAndSpanExpected =
        rawtext2
            + inlineSpanOPEN
            + inlineTransLink.toString()
            + linkOPEN
            + linkSpanOPEN
            + plainVanillaPlain
            + linkSpanCLOSE
            + linkCLOSE
            + inlineSpanCLOSE;
    assertEquals(convertedWithinLinkAndSpanExpected, convertedToHTMLMarkup);
    // Muliple links
    String translatedWithinMultipleLinks =
        translatedWithinLink + translatedWithinLinkAndSpan + translatedWithinLinkAndSpan;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedWithinMultipleLinks, inlineTranslationURLBuilder, inlineTrans);
    String convertedWithinMultipleLinksExpected =
        convertedWithinLinkExpected
            + convertedWithinLinkAndSpanExpected
            + convertedWithinLinkAndSpanExpected;
    assertEquals(convertedWithinMultipleLinksExpected, convertedToHTMLMarkup);
    // Input elements
    String inputOPEN = "<input type='submit' class=\"bluber\" value=\"";
    String inputCLOSE = "\" />";
    String translatedWithinInput = inputOPEN + plainVanillaWrapped + inputCLOSE + rawtext1;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedWithinInput, inlineTranslationURLBuilder, inlineTrans);
    String convertedWithinInputExpected =
        inlineSpanOPEN
            + inlineTransLink.toString()
            + inputOPEN
            + plainVanillaPlain
            + inputCLOSE
            + inlineSpanCLOSE
            + rawtext1;
    assertEquals(convertedWithinInputExpected, convertedToHTMLMarkup);
    // checkbox elements
    String checkboxOPEN = "<input type='submit' class=\"bluber\" type=\"checkbox\" value=\"";
    String checkboxCLOSE = "\" />";
    String translatedWithinCheckbox = checkboxOPEN + plainVanillaWrapped + checkboxCLOSE + rawtext1;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedWithinCheckbox, inlineTranslationURLBuilder, inlineTrans);
    String convertedWithinCheckboxExpected =
        checkboxOPEN + plainVanillaPlain + checkboxCLOSE + rawtext1;
    assertEquals(convertedWithinCheckboxExpected, convertedToHTMLMarkup);
    // Input and links mixed
    String translatedWithinMultipleLinksAndInput =
        translatedWithinLink
            + rawtext1
            + translatedWithinInput
            + translatedWithinLinkAndSpan
            + rawtext2
            + translatedWithinInput
            + translatedWithinLinkAndSpan;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedWithinMultipleLinksAndInput, inlineTranslationURLBuilder, inlineTrans);
    String convertedWithinMultipleLinksAndInputExpected =
        convertedWithinLinkExpected
            + rawtext1
            + convertedWithinInputExpected
            + convertedWithinLinkAndSpanExpected
            + rawtext2
            + convertedWithinInputExpected
            + convertedWithinLinkAndSpanExpected;
    assertEquals(convertedWithinMultipleLinksAndInputExpected, convertedToHTMLMarkup);
    // Within element attribute
    String attributeOPEN = "<a href='sdfsdf' title=\"";
    String attributeCLOSE = "\" class=\"b_bluber\">hello world</a>";
    String translatedWithinAttribute =
        attributeOPEN + plainVanillaWrapped + attributeCLOSE + rawtext1;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedWithinAttribute, inlineTranslationURLBuilder, inlineTrans);
    String convertedWithinAttributeExpected =
        attributeOPEN + plainVanillaPlain + attributeCLOSE + rawtext1;
    assertEquals(convertedWithinAttributeExpected, convertedToHTMLMarkup);
    // Ultimate test
    String translatedUltimate =
        translatedWithinMultipleLinksAndInput
            + rawtext1
            + translatedWithinAttribute
            + translatedWithinMultipleLinksAndInput
            + rawtext2
            + translatedWithinInput
            + translatedWithinLinkAndSpan;
    convertedToHTMLMarkup =
        InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(
            translatedUltimate, inlineTranslationURLBuilder, inlineTrans);
    String convertedUltimateExpected =
        convertedWithinMultipleLinksAndInputExpected
            + rawtext1
            + convertedWithinAttributeExpected
            + convertedWithinMultipleLinksAndInputExpected
            + rawtext2
            + convertedWithinInputExpected
            + convertedWithinLinkAndSpanExpected;
    assertEquals(convertedUltimateExpected, convertedToHTMLMarkup);

    // don't do inline translation markup
    i18nMgr.setMarkLocalizedStringsEnabled(null, false);
  }

  /** Test method i18nManager.getLocalizedString() with resolvePropertiesInternalKeys() */
  @Test
  public void testResolvePropertiesInternalKeys() {
    String bundleName = "org.olat.core.util.i18n.junittestdata.subtest";
    Locale locale = Locale.GERMAN;
    // first tests with cache enabled
    i18nMgr.setCachingEnabled(true);
    assertEquals(
        "Hello world, this is just a test",
        i18nMgr.getLocalizedString(bundleName, "recursive.test", null, locale, false, true));
    assertEquals(
        "Hello world, this is just a test (dont translate it)",
        i18nMgr.getLocalizedString(bundleName, "recursive.test2", null, locale, false, true));
    // test with . and ,
    assertEquals(
        "Hello world, this is just a test. Really dont translate it, because this is just a test.",
        i18nMgr.getLocalizedString(bundleName, "recursive.test3", null, locale, false, true));
    // a combined word
    assertEquals(
        "Hello world, this is obersuperduper",
        i18nMgr.getLocalizedString(bundleName, "recursive.test4", null, locale, false, true));
    // a endless loop that can't be resolved
    assertEquals(
        "Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop $org.olat.core.util.i18n.junittestdata.subtest:recursive.test5",
        i18nMgr.getLocalizedString(bundleName, "recursive.test5", null, locale, false, true));

    // same tests without cache
    i18nMgr.setCachingEnabled(false);
    assertEquals(
        "Hello world, this is just a test",
        i18nMgr.getLocalizedString(bundleName, "recursive.test", null, locale, false, true));
    assertEquals(
        "Hello world, this is just a test (dont translate it)",
        i18nMgr.getLocalizedString(bundleName, "recursive.test2", null, locale, false, true));
    // test with . and ,
    assertEquals(
        "Hello world, this is just a test. Really dont translate it, because this is just a test.",
        i18nMgr.getLocalizedString(bundleName, "recursive.test3", null, locale, false, true));
    // a combined word
    assertEquals(
        "Hello world, this is obersuperduper",
        i18nMgr.getLocalizedString(bundleName, "recursive.test4", null, locale, false, true));
    // a endless loop that can't be resolved
    assertEquals(
        "Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop Loop $org.olat.core.util.i18n.junittestdata.subtest:recursive.test5",
        i18nMgr.getLocalizedString(bundleName, "recursive.test5", null, locale, false, true));
    // note: it's ok that the results differ here. Important is that the loop
    // eventually terminates. when caching is enabled, the resulst are
    // accumulated from the previous iteration, this is different in non-caching
    // mode. But this should not have a negative effect on real-world scenarios

    i18nMgr.setCachingEnabled(true);
  }

  /**
   * Description:<br>
   * Dummy URL builder
   *
   * <p>Initial Date: 22.09.2008 <br>
   *
   * @author gnaegi
   */
  private class jUnitURLBuilder extends URLBuilder {
    public jUnitURLBuilder() {
      super(null, null, null, null);
    }

    public void buildURI(StringOutput buf, String[] keys, String[] values) {
      buf.append("http://do.test.com");
    }
  }
}
/** @author Felix Jost */
public class VelocityRenderDecorator implements Closeable {
  private static final OLog log = Tracing.createLoggerFor(VelocityRenderDecorator.class);

  public static final String PARAM_CHELP_BUNDLE = "chelpbundle";
  private VelocityContainer vc;
  private Renderer renderer;
  private final boolean isIframePostEnabled;
  private StringOutput target;

  /**
   * @param renderer
   * @param vc
   */
  public VelocityRenderDecorator(Renderer renderer, VelocityContainer vc, StringOutput target) {
    this.renderer = renderer;
    this.vc = vc;
    this.target = target;
    this.isIframePostEnabled = renderer.getGlobalSettings().getAjaxFlags().isIframePostEnabled();
  }

  @Override
  public void close() throws IOException {
    vc = null;
    target = null;
    renderer = null;
  }

  /**
   * @param prefix e.g. abc for "abc647547326" and so on
   * @return an prefixed id (e.g. f23748932) which is unique in the current render tree.
   */
  public StringOutput getId(String prefix) {
    StringOutput sb = new StringOutput(16);
    sb.append("o_s").append(prefix).append(vc.getDispatchID());
    return sb;
  }

  public static String getId(String prefix, VelocityContainer vc) {
    StringOutput sb = StringOutputPool.allocStringBuilder(24);
    sb.append("o_s").append(prefix).append(vc.getDispatchID());
    return StringOutputPool.freePop(sb);
  }

  public String getUniqueId() {
    return Long.toString(CodeHelper.getRAMUniqueID());
  }

  /** @return the componentid (e.g.) o_c32645732 */
  public StringOutput getCId() {
    StringOutput sb = new StringOutput(16);
    sb.append("o_c").append(vc.getDispatchID());
    return sb;
  }

  public String getUuid() {
    return UUID.randomUUID().toString().replace("-", "");
  }

  /**
   * @param command
   * @return e.g. /olat/auth/1:2:3:cid:com/
   */
  public StringOutput commandURIbg(String command) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildURI(
            sb,
            new String[] {VelocityContainer.COMMAND_ID},
            new String[] {command},
            isIframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
    return sb;
  }

  /**
   * renderer a target="oaa" if ajax-mode is on, otherwise returns an empty string
   *
   * @return
   */
  public StringOutput bgTarget() {
    StringOutput sb = new StringOutput(16);
    if (isIframePostEnabled) {
      renderer.getUrlBuilder().appendTarget(sb);
    }
    return sb;
  }

  /**
   * FIXME:fj:b search occurences for $r.commandURI and try to replace them with $r.link(...) or
   * such
   *
   * @param command
   * @return
   */
  public StringOutput commandURI(String command) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildURI(sb, new String[] {VelocityContainer.COMMAND_ID}, new String[] {command});
    return sb;
  }

  /**
   * Creates a java script fragment to execute a background request. In ajax mode the request uses
   * the ajax asynchronous methods, in legacy mode it uses a standard document.location.request
   *
   * @param command
   * @param paramKey
   * @param paramValue
   * @return
   */
  public StringOutput javaScriptBgCommand(String command, String paramKey, String paramValue) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildJavaScriptBgCommand(
            sb,
            new String[] {VelocityContainer.COMMAND_ID, paramKey},
            new String[] {command, paramValue},
            isIframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
    return sb;
  }

  /**
   * Creates a java script fragment to execute a background request. In ajax mode the request uses
   * the ajax asynchronous methods, in legacy mode it uses a standard document.location.request
   *
   * @param command
   * @return
   */
  public StringOutput javaScriptBgCommand(String command) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildJavaScriptBgCommand(
            sb,
            new String[] {VelocityContainer.COMMAND_ID},
            new String[] {command},
            isIframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
    return sb;
  }

  /**
   * Use it to create the action for a handmade form in a velocity template, e.g. '<form
   * method="post" action="$r.formURIgb("viewswitch")">'
   *
   * @param command
   * @return
   */
  public StringOutput formURIbg(String command) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildURI(
            sb,
            new String[] {VelocityContainer.COMMAND_ID},
            new String[] {command},
            isIframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
    return sb;
  }

  /**
   * Use it to create the forced non-ajax action for a handmade form in a velocity template, e.g.
   * '<form method="post" action="$r.formURIgb("viewswitch")">'
   *
   * @param command
   * @return
   */
  public StringOutput formURI(String command) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildURI(sb, new String[] {VelocityContainer.COMMAND_ID}, new String[] {command});
    return sb;
  }

  /**
   * @param command
   * @return
   */
  public StringOutput commandURI(String command, String paramKey, String paramValue) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildURI(
            sb,
            new String[] {VelocityContainer.COMMAND_ID, paramKey},
            new String[] {command, paramValue});
    return sb;
  }

  /**
   * @param command
   * @return
   */
  public StringOutput commandURIbg(String command, String paramKey, String paramValue) {
    StringOutput sb = new StringOutput(100);
    renderer
        .getUrlBuilder()
        .buildURI(
            sb,
            new String[] {VelocityContainer.COMMAND_ID, paramKey},
            new String[] {command, paramValue},
            isIframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
    return sb;
  }

  /**
   * should be called within the main .html template after the <head>tag. gets some js/css/onLoad
   * code from the component to render/work correctly.
   *
   * @return
   */
  public StringOutput renderBodyOnLoadJSFunctionCall() {
    StringOutput sb = new StringOutput(100);
    renderer.renderBodyOnLoadJSFunctionCall(sb, vc);
    return sb;
  }

  /**
   * should be called within the main .html template after the <head>tag. gets some js/css/onLoad
   * code from the component to render/work correctly.
   *
   * @return
   */
  public StringOutput renderHeaderIncludes() {
    StringOutput sb = new StringOutput(100);
    renderer.renderHeaderIncludes(sb, vc);
    return sb;
  }

  /**
   * Note: use only rarely - e.g. for static redirects to login screen or to a special dispatcher.
   * Renders a uri which is mounted to the webapp/ directory of your webapplication.
   *
   * <p>For static references (e.g. images which cannot be delivered using css): use renderStaticURI
   * instead!
   */
  public StringOutput relLink(String URI) {
    StringOutput sb = new StringOutput(100);
    Renderer.renderNormalURI(sb, URI);
    return sb;
  }

  /**
   * e.g. "images/somethingicannotdowithcss.jpg" ->
   * /olat/raw/61x/images/somethingicannotdowithcss.jpg" with /olat/raw/61x/ mounted to
   * webapp/static directory of your webapp
   *
   * @param URI
   * @return
   */
  public StringOutput staticLink(String URI) {
    StringOutput sb = new StringOutput(100);
    Renderer.renderStaticURI(sb, URI);
    return sb;
  }

  /**
   * e.g. "/olat/"
   *
   * @return
   */
  public String relWinLink() {
    return renderer.getUriPrefix();
  }

  /**
   * @param componentName
   * @return
   */
  public StringOutput render(String componentName) {
    return doRender(componentName, null);
  }

  /**
   * used to position help icon inside div-class o_chelp_wrapper
   *
   * @param packageName
   * @param pageName
   * @param hoverTextKey
   * @return
   */
  public StringOutput contextHelpWithWrapper(
      String packageName, String pageName, String hoverTextKey) {
    StringOutput sb = new StringOutput(100);
    if (ContextHelpModule.isContextHelpEnabled()) {
      sb.append("<span class=\"o_chelp_wrapper\">");
      sb.append(contextHelp(packageName, pageName, hoverTextKey));
      sb.append("</span>");
    }
    return sb;
  }

  /**
   * @param packageName
   * @param pageName
   * @param hoverTextKey
   * @return
   */
  public StringOutput contextHelp(String packageName, String pageName, String hoverTextKey) {
    StringOutput sb = new StringOutput(100);
    if (ContextHelpModule.isContextHelpEnabled()) {
      String hooverText = renderer.getTranslator().translate(hoverTextKey);
      if (hooverText != null) {
        hooverText = StringEscapeUtils.escapeHtml(hooverText);
      }
      sb.append("<a href=\"javascript:");
      sb.append(contextHelpJSCommand(packageName, pageName));
      sb.append("\" title=\"")
          .append(hooverText)
          .append("\" class=\"o_chelp\"><i class='o_icon o_icon_help'></i> ");
      sb.append(renderer.getTranslator().translate("help"));
      sb.append("</a>");
    }
    return sb;
  }

  public StringOutput contextHelpWithWrapper(String page) {
    StringOutput sb = new StringOutput(192);
    if (ContextHelpModule.isContextHelpEnabled()) {
      HelpModule helpModule = CoreSpringFactory.getImpl(HelpModule.class);
      Locale locale = renderer.getTranslator().getLocale();
      String url = helpModule.getHelpProvider().getURL(locale, page);
      if (url != null) {
        sb.append("<span class=\"o_chelp_wrapper\">")
            .append("<a href=\"")
            .append(url)
            .append("\" class=\"o_chelp\" target=\"_blank\"><i class='o_icon o_icon_help'></i> ")
            .append(renderer.getTranslator().translate("help"))
            .append("</a></span>");
      }
    }
    return sb;
  }

  /**
   * Create a js command to open a specific context help page
   *
   * @param packageName
   * @param pageName
   * @return
   */
  public StringOutput contextHelpJSCommand(String packageName, String pageName) {
    StringOutput sb = new StringOutput(100);
    if (ContextHelpModule.isContextHelpEnabled()) {
      String langCode = renderer.getTranslator().getLocale().toString();
      sb.append("contextHelpWindow('");
      Renderer.renderNormalURI(sb, "help/");
      sb.append(langCode).append("/").append(packageName).append("/").append(pageName);
      sb.append("')");
    }
    return sb;
  }

  /**
   * Create a link that can be used within a context help page to link to another context help page
   * from the same package.
   *
   * @param pageName e.g. "my-page.html"
   * @return
   */
  public StringOutput contextHelpRelativeLink(String pageName) {
    return contextHelpRelativeLink(null, pageName);
  }

  /**
   * Create a link that can be used within a context help page to link to another context help page
   * from another package. As link text the page title is used.
   *
   * @param bundleName e.g. "org.olat.core"
   * @param pageName e.g. "my-page.html"
   * @return
   */
  public StringOutput contextHelpRelativeLink(String bundleName, String pageName) {
    String linkText;
    int lastDotPos = pageName.lastIndexOf(".");
    if (lastDotPos != -1) {
      Translator pageTrans = renderer.getTranslator();
      if (bundleName != null) {
        Locale locale = pageTrans.getLocale();
        pageTrans = new PackageTranslator(bundleName, locale);
      }
      linkText = pageTrans.translate("chelp." + pageName.subSequence(0, lastDotPos) + ".title");
    } else {
      linkText = pageName; // fallback
    }
    return contextHelpRelativeLink(bundleName, pageName, linkText);
  }

  /**
   * Create a link that can be used within a context help page to link to another context help page
   * from another package. The link text can be specified as a thirt attribute.
   *
   * @param bundleName e.g. "org.olat.core"
   * @param pageName e.g. "my-page.html"
   * @return
   */
  public StringOutput contextHelpRelativeLink(String bundleName, String pageName, String linkText) {
    StringOutput sb = new StringOutput(100);
    if (ContextHelpModule.isContextHelpEnabled()) {
      sb.append("<a href=\"");
      if (bundleName == null) {
        renderer
            .getUrlBuilder()
            .buildURI(
                sb,
                new String[] {VelocityContainer.COMMAND_ID},
                new String[] {pageName},
                isIframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
      } else {
        renderer
            .getUrlBuilder()
            .buildURI(
                sb,
                new String[] {VelocityContainer.COMMAND_ID, PARAM_CHELP_BUNDLE},
                new String[] {pageName, bundleName},
                isIframePostEnabled ? AJAXFlags.MODE_TOBGIFRAME : AJAXFlags.MODE_NORMAL);
      }
      sb.append("\" ");
      if (isIframePostEnabled) {
        renderer.getUrlBuilder().appendTarget(sb);
      }
      sb.append(">");
      sb.append(linkText);
      sb.append("</a>");
    }
    return sb;
  }

  /**
   * @param componentName
   * @param arg1
   * @return
   */
  public StringOutput render(String componentName, String arg1) {
    return doRender(componentName, new String[] {arg1});
  }

  /**
   * @param componentName
   * @param arg1
   * @param arg2
   * @return
   */
  public StringOutput render(String componentName, String arg1, String arg2) {
    return doRender(componentName, new String[] {arg1, arg2});
  }

  /**
   * Parametrised translator
   *
   * @param key The translation key
   * @param arg1 The argument list as a string array
   * @return
   */
  public String translate(String key, String[] arg1) {
    Translator trans = renderer.getTranslator();
    if (trans == null) return "{Translator is null: key_to_translate=" + key + "}";
    String res = trans.translate(key, arg1);
    if (res == null)
      return "?? key not found to translate: key_to_translate="
          + key
          + " / trans info:"
          + trans
          + "}";
    return res;
  }

  /**
   * Wrapper to make it possible to use the parametrised translator from within the velocity
   * container
   *
   * @param key The translation key
   * @param arg1 The argument list as a list of strings
   * @return the translated string
   */
  public String translate(String key, List<String> arg1) {
    return translate(key, arg1.toArray(new String[arg1.size()]));
  }

  /**
   * Wrapper to make it possible to use the parametrised translator from within the velocity
   * container
   *
   * @param key The translation key
   * @param arg1 The argument sd string
   * @return the translated string
   */
  public String translate(String key, String arg1) {
    return translate(key, new String[] {arg1});
  }

  /**
   * Method to translate a key that comes from another package. This should be used rarely. When a
   * key is used withing multiple packages is is usually better to use a fallback translator or to
   * move the key to the default packages.
   *
   * <p>Used in context help system
   *
   * @param bundleName the package name, e.g. 'org.olat.core'
   * @param key the key, e.g. 'my.key'
   * @param args optional arguments, null if not used
   * @return
   */
  public String translateWithPackage(String bundleName, String key, String[] args) {
    Translator pageTrans = renderer.getTranslator();
    if (pageTrans == null) return "{Translator is null: key_to_translate=" + key + "}";
    Locale locale = pageTrans.getLocale();
    Translator tempTrans = new PackageTranslator(bundleName, locale);
    String result = tempTrans.translate(key, args);
    if (result == null) {
      return "{Invalid bundle name: " + bundleName + " and key: " + key + "}";
    }
    return result;
  }

  /**
   * Method to translate a key that comes from another package. This should be used rarely. When a
   * key is used withing multiple packages is is usually better to use a fallback translator or to
   * move the key to the default packages.
   *
   * @param bundleName the package name, e.g. 'org.olat.core'
   * @param key the key, e.g. 'my.key'
   * @return
   */
  public String translateWithPackage(String bundleName, String key) {
    return translateWithPackage(bundleName, key, null);
  }

  /**
   * escapes " entities in \"
   *
   * @param in the string to convert
   * @deprecated please use escapeHtml.
   * @return the escaped string
   */
  public String escapeDoubleQuotes(String in) {
    return Formatter.escapeDoubleQuotes(in).toString();
  }

  /** Escapes the characters in a String for JavaScript use. */
  public String escapeJavaScript(String str) {
    return StringHelper.escapeJavaScript(str);
  }

  /**
   * Escapes the characters in a String using HTML entities.
   *
   * @param str
   * @return
   */
  public String escapeHtml(String str) throws IOException {
    if (str == null) {
      return "";
    }
    return StringHelper.escapeHtml(str);
  }

  public String xssScan(String str) {
    if (str == null) {
      return "";
    }
    OWASPAntiSamyXSSFilter filter = new OWASPAntiSamyXSSFilter();
    return filter.filter(str);
  }

  public String filterHtml(String str) {
    if (str == null) {
      return "";
    }
    return FilterFactory.getHtmlTagsFilter().filter(str);
  }

  /**
   * @param key
   * @return
   */
  public String translate(String key) {
    Translator trans = renderer.getTranslator();
    if (trans == null) return "{Translator is null: key_to_translate=" + key + "}";
    String res = trans.translate(key);
    if (res == null)
      return "?? key not found to translate: key_to_translate="
          + key
          + " / trans info:"
          + trans
          + "}";
    return res;
  }

  /**
   * Translates and escapesHTML. It assumes that the HTML attribute value should be enclosed in
   * double quotes.
   *
   * @param key
   * @return
   */
  public String translateInAttribute(String key) {
    return StringEscapeUtils.escapeHtml(translate(key));
  }

  /** @return current language code as found in (current)Locale.toString() method */
  public String getLanguageCode() {
    Locale currentLocale = I18nManager.getInstance().getCurrentThreadLocale();
    return currentLocale.toString();
  }

  /**
   * renders the component. if the component cannot be found, there is no error, but an empty String
   * is returned. Not recommended to use normally, but rather use @see render(String componentName)
   *
   * @param componentName
   * @return
   */
  public StringOutput renderForce(String componentName) {
    Component source = renderer.findComponent(componentName);
    StringOutput sb;
    if (source == null) {
      sb = new StringOutput(1);
    } else if (target == null) {
      sb = new StringOutput(10000);
      renderer.render(source, sb, null);
    } else {
      renderer.render(source, target, null);
    }
    return new StringOutput(1);
  }

  private StringOutput doRender(String componentName, String[] args) {
    Component source = renderer.findComponent(componentName);
    StringOutput sb;
    if (source == null) {
      sb = new StringOutput(128);
      sb.append(
          ">>>>>>>>>>>>>>>>>>>>>>>>>> component "
              + componentName
              + " could not be found to be rendered!");
    } else if (target == null) {
      sb = new StringOutput(10000);
      renderer.render(source, sb, args);
    } else {
      sb = new StringOutput(1);
      renderer.render(source, target, args);
    }
    return sb;
  }

  /**
   * @param componentName
   * @return true if the component with name componentName is a child of the current container. Used
   *     to "if" the render instruction "$r.render(componentName)" if it is not known beforehand
   *     whether the component is there or not.
   */
  public boolean available(String componentName) {
    Component source = renderer.findComponent(componentName);
    return (source != null);
  }

  /**
   * @param componentName
   * @return true if the component with name componentName is a child of the current container and
   *     if this component is visible
   */
  public boolean visible(String componentName) {
    Component source = renderer.findComponent(componentName);
    return (source != null && source.isVisible());
  }

  /**
   * Return the component
   *
   * @param componentName
   * @return
   */
  public Component getComponent(String componentName) {
    Component source = renderer.findComponent(componentName);
    return source;
  }

  /**
   * returns an object from the context of velocity
   *
   * @param key
   * @return
   */
  public Object get(String key) {
    return vc.getContext().get(key);
  }

  public boolean absent(String key) {
    return !vc.getContext().containsKey(key);
  }

  public String formatDate(Date date) {
    Formatter f = Formatter.getInstance(renderer.getTranslator().getLocale());
    return f.formatDate(date);
  }

  public String formatDateAndTime(Date date) {
    Formatter f = Formatter.getInstance(renderer.getTranslator().getLocale());
    return f.formatDateAndTime(date);
  }

  public String formatBytes(long bytes) {
    return Formatter.formatBytes(bytes);
  }

  /**
   * Wrapp given html code with a wrapper an add code to transform latex formulas to nice visual
   * characters on the client side. The latex formulas must be within an HTML element that has the
   * class 'math' attached.
   *
   * @param htmlFragment A html element that might contain an element that has a class 'math' with
   *     latex formulas
   * @return
   */
  public static String formatLatexFormulas(String htmlFragment) {
    return Formatter.formatLatexFormulas(htmlFragment);
  }

  /**
   * Search in given text fragment for URL's and surround them with clickable HTML link objects.
   *
   * @param textFragment
   * @return text with clickable links
   */
  public static String formatURLsAsLinks(String textFragment) {
    return Formatter.formatURLsAsLinks(textFragment);
  }

  /**
   * Strips all HTML tags from the source string.
   *
   * @param source Source
   * @return Source without HTML tags.
   */
  public static String filterHTMLTags(String source) {
    Filter htmlTagsFilter = FilterFactory.getHtmlTagsFilter();
    return htmlTagsFilter.filter(source);
  }

  /**
   * Get the icon css class that represents the filetype based on the file name
   *
   * @param filename
   * @return The css class for the file or a default css class
   */
  public static String getFiletypeIconCss(String filename) {
    return CSSHelper.createFiletypeIconCssClassFor(filename);
  }

  /**
   * Returns true when debug mode is configured, false otherwhise
   *
   * @return
   */
  public boolean isDebuging() {
    return Settings.isDebuging();
  }

  /**
   * To inject licenses (the NOTICE.TXT) in the help
   *
   * @return
   */
  public String getLicences() {
    String licenses = "Not found";
    InputStream licensesStream =
        LoginAuthprovidersController.class.getResourceAsStream("../../../NOTICE.TXT");
    if (licensesStream != null) {
      try {
        licenses = IOUtils.toString(licensesStream);
      } catch (IOException e) {
        log.error("", e);
      } finally {
        IOUtils.closeQuietly(licensesStream);
      }
    }
    return licenses;
  }

  public String getVersion() {
    return Settings.getVersion();
  }

  public Languages getLanguages() {
    I18nManager i18nMgr = I18nManager.getInstance();
    Collection<String> enabledKeysSet = I18nModule.getEnabledLanguageKeys();
    Map<String, String> langNames = new HashMap<String, String>();
    Map<String, String> langTranslators = new HashMap<String, String>();
    String[] enabledKeys = ArrayHelper.toArray(enabledKeysSet);
    String[] names = new String[enabledKeys.length];
    for (int i = 0; i < enabledKeys.length; i++) {
      String key = enabledKeys[i];
      String langName = i18nMgr.getLanguageInEnglish(key, I18nModule.isOverlayEnabled());
      langNames.put(key, langName);
      names[i] = langName;
      String author = i18nMgr.getLanguageAuthor(key);
      langTranslators.put(key, author);
    }
    ArrayHelper.sort(enabledKeys, names, true, true, true);
    return new Languages(enabledKeys, langNames, langTranslators);
  }

  public static class Languages {
    private final String[] enabledKeys;
    private final Map<String, String> langNames;
    private final Map<String, String> langTranslators;

    public Languages(
        String[] enabledKeys, Map<String, String> langNames, Map<String, String> langTranslators) {
      this.enabledKeys = enabledKeys;
      this.langNames = langNames;
      this.langTranslators = langTranslators;
    }

    public String[] getEnabledKeys() {
      return enabledKeys;
    }

    public Map<String, String> getLangNames() {
      return langNames;
    }

    public Map<String, String> getLangTranslators() {
      return langTranslators;
    }
  }
}
/** @author Mike Stock Comment: Initial Date: 04.06.2003 */
public class LocalizedXSLTransformer {
  private static ConcurrentHashMap<String, LocalizedXSLTransformer> instanceHash =
      new ConcurrentHashMap<String, LocalizedXSLTransformer>(5);
  private static OLog log = Tracing.createLoggerFor(LocalizedXSLTransformer.class);
  private static EntityResolver er = new IMSEntityResolver();
  private static VelocityEngine velocityEngine;

  static {
    // init velocity engine
    Properties p = null;
    try {
      velocityEngine = new VelocityEngine();
      p = new Properties();
      p.setProperty(
          RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
          "org.apache.velocity.runtime.log.SimpleLog4JLogSystem");
      p.setProperty("runtime.log.logsystem.log4j.category", "syslog");
      velocityEngine.init(p);
    } catch (final Exception e) {
      throw new OLATRuntimeException("config error with velocity properties::" + p.toString(), e);
    }
  }

  private final Translator pT;
  private Transformer transformer;
  /** <code>RESULTS2HTML</code> */
  private static final String XSLFILENAME = "results2html_generic.xsl";

  /**
   * Private constructor, use getInstance to get an instance of the LocalizedXSLTransformer
   *
   * @param trans
   */
  private LocalizedXSLTransformer(final Translator trans) {
    pT = trans;
    initTransformer();
  }

  /**
   * Get a localized transformer instance.
   *
   * @param locale The locale for this transformer instance
   * @return A localized transformer
   */
  // cluster_ok only in VM
  public static synchronized LocalizedXSLTransformer getInstance(final Locale locale) {
    LocalizedXSLTransformer instance =
        instanceHash.get(I18nManager.getInstance().getLocaleKey(locale));
    if (instance == null) {
      final Translator trans =
          Util.createPackageTranslator(QTIResultDetailsController.class, locale);
      final LocalizedXSLTransformer newInstance = new LocalizedXSLTransformer(trans);
      instance =
          instanceHash.putIfAbsent(
              I18nManager.getInstance().getLocaleKey(locale),
              newInstance); // see javadoc of ConcurrentHashMap
      if (instance == null) { // newInstance was put into the map
        instance = newInstance;
      }
    }
    return instance;
  }

  /**
   * Render with a localized stylesheet. The localized stylesheet is addressed by its name with
   * appended locale. E.g. mystyle.xsl in DE locale is addressed by mystyle_de.xsl
   *
   * @param node The node to render
   * @param styleSheetName The stylesheet to use.
   * @return Results of XSL transformation
   */
  private StringBuilder render(final Element node) {
    try {
      Document doc = node.getDocument();
      if (doc == null) {
        doc = new DOMDocument();
        doc.add(node);
      }
      final DocumentSource xmlsource = new DocumentSource(node);

      // ByteArrayOutputStream baos = new ByteArrayOutputStream();
      final StringWriter sw = new StringWriter();
      final StreamResult result = new StreamResult(sw);
      synchronized (transformer) { // o_clusterOK by:fj transformer is per vm
        transformer.transform(xmlsource, result);
      }
      final String res = sw.toString();
      return new StringBuilder(res); // .append(result.getOutputStream());
    } catch (final Exception e) {
      throw new OLATRuntimeException(LocalizedXSLTransformer.class, "Error transforming XML.", e);
    }
  }

  /**
   * Render results processing document
   *
   * @param doc The <results/>document
   * @return transformation results
   */
  public StringBuilder renderResults(final Document doc) {
    return render(doc.getRootElement());
  }

  /** Helper to create XSLT transformer for this instance */
  private void initTransformer() {
    // build new transformer
    final InputStream xslin =
        getClass().getResourceAsStream("/org/olat/ims/resources/xsl/" + XSLFILENAME);
    // translate xsl with velocity
    final Context vcContext = new VelocityContext();
    vcContext.put("t", pT);
    vcContext.put("staticPath", StaticMediaDispatcher.createStaticURIFor(""));
    String xslAsString = "";
    try {
      xslAsString = slurp(xslin);
    } catch (final IOException e) {
      log.error("Could not convert xsl to string!", e);
    }
    final String replacedOutput = evaluateValue(xslAsString, vcContext);
    final TransformerFactory tfactory = TransformerFactory.newInstance();
    XMLReader reader;
    try {
      reader = XMLReaderFactory.createXMLReader();
      reader.setEntityResolver(er);
      final Source xsltsource =
          new SAXSource(reader, new InputSource(new StringReader(replacedOutput)));
      this.transformer = tfactory.newTransformer(xsltsource);
    } catch (final SAXException e) {
      throw new OLATRuntimeException("Could not initialize transformer!", e);
    } catch (final TransformerConfigurationException e) {
      throw new OLATRuntimeException("Could not initialize transformer (wrong config)!", e);
    }
  }

  /**
   * Takes String with template and fills values from Translator in Context
   *
   * @param valToEval String with variables to replace
   * @param vcContext velocity context containing a translator in this case
   * @return input String where values from context were replaced
   */
  private String evaluateValue(final String valToEval, final Context vcContext) {
    final StringWriter evaluatedValue = new StringWriter();
    // evaluate inputFieldValue to get a concatenated string
    try {
      velocityEngine.evaluate(vcContext, evaluatedValue, "vcUservalue", valToEval);
    } catch (final ParseErrorException e) {
      log.error("parsing of values in xsl-file of LocalizedXSLTransformer not possible!", e);
      return "ERROR";
    } catch (final MethodInvocationException e) {
      log.error("evaluating of values in xsl-file of LocalizedXSLTransformer not possible!", e);
      return "ERROR";
    } catch (final ResourceNotFoundException e) {
      log.error("xsl-file of LocalizedXSLTransformer not found!", e);
      return "ERROR";
    } catch (final IOException e) {
      log.error("could not read xsl-file of LocalizedXSLTransformer!", e);
      return "ERROR";
    }
    return evaluatedValue.toString();
  }

  /**
   * convert xsl InputStream to String
   *
   * @param in
   * @return xsl as String
   * @throws IOException
   */
  private static String slurp(final InputStream in) throws IOException {
    final StringBuffer out = new StringBuffer();
    final byte[] b = new byte[4096];
    for (int n; (n = in.read(b)) != -1; ) {
      out.append(new String(b, 0, n));
    }
    return out.toString();
  }
}
/**
 * Lucene document mapper.
 *
 * @author Christian Guretzki
 */
public class WikiPageDocument extends OlatDocument {

  private static final OLog log = Tracing.createLoggerFor(WikiPageDocument.class);
  private static final DummyDataHandler DUMMY_DATA_HANDLER = new DummyDataHandler();

  private static BaseSecurity identityManager;

  public WikiPageDocument() {
    super();
    identityManager = BaseSecurityManager.getInstance();
  }

  public static Document createDocument(
      final SearchResourceContext searchResourceContext, final WikiPage wikiPage) {
    final WikiPageDocument wikiPageDocument = new WikiPageDocument();

    final long userId = wikiPage.getInitalAuthor();
    if (userId != 0) {
      final Identity identity = identityManager.loadIdentityByKey(Long.valueOf(userId));
      wikiPageDocument.setAuthor(identity.getName());
    }
    wikiPageDocument.setTitle(wikiPage.getPageName());
    wikiPageDocument.setContent(getContent(wikiPage));
    wikiPageDocument.setCreatedDate(new Date(wikiPage.getCreationTime()));
    wikiPageDocument.setLastChange(new Date(wikiPage.getModificationTime()));
    wikiPageDocument.setResourceUrl(searchResourceContext.getResourceUrl());
    wikiPageDocument.setDocumentType(searchResourceContext.getDocumentType());
    wikiPageDocument.setCssIcon("o_wiki_icon");
    wikiPageDocument.setParentContextType(searchResourceContext.getParentContextType());
    wikiPageDocument.setParentContextName(searchResourceContext.getParentContextName());

    if (log.isDebug()) {
      log.debug(wikiPageDocument.toString());
    }
    return wikiPageDocument.getLuceneDocument();
  }

  private static String getContent(final WikiPage wikiPage) {
    try {
      final ParserInput input = new ParserInput();
      input.setWikiUser(null);
      input.setAllowSectionEdit(false);
      input.setDepth(2);
      input.setContext("");
      input.setLocale(Locale.ENGLISH);
      input.setTopicName("dummy");
      input.setUserIpAddress("0.0.0.0");
      input.setDataHandler(DUMMY_DATA_HANDLER);
      input.setVirtualWiki("/olat");

      final AbstractParser parser = new JFlexParser(input);
      final ParserDocument parsedDoc = parser.parseHTML(wikiPage.getContent());
      final String parsedContent = parsedDoc.getContent();
      final String filteredContent =
          FilterFactory.getHtmlTagAndDescapingFilter().filter(parsedContent);
      return filteredContent;
    } catch (final Exception e) {
      e.printStackTrace();
      log.error("", e);
      return wikiPage.getContent();
    }
  }

  private static class DummyDataHandler implements DataHandler {

    @Override
    public boolean exists(final String virtualWiki, final String topic) {
      return true;
    }

    @Override
    public Topic lookupTopic(
        final String virtualWiki,
        final String topicName,
        final boolean deleteOK,
        final Object transactionObject)
        throws Exception {
      return null;
    }

    @Override
    public WikiFile lookupWikiFile(final String virtualWiki, final String topicName)
        throws Exception {
      return null;
    }
  }
}
/**
 * Description:<br>
 * This indexer indexes the context sensitive help system
 *
 * <p>Initial Date: 05.11.2008 <br>
 *
 * @author gnaegi
 */
public class ContextHelpIndexer extends AbstractHierarchicalIndexer {
  private static final OLog log = Tracing.createLoggerFor(ContextHelpIndexer.class);

  /**
   * @see org.olat.search.service.indexer.Indexer#checkAccess(org.olat.core.id.context.ContextEntry,
   *     org.olat.core.id.context.BusinessControl, org.olat.core.id.Identity,
   *     org.olat.core.id.Roles)
   */
  public boolean checkAccess(
      ContextEntry contextEntry, BusinessControl businessControl, Identity identity, Roles roles) {
    // context help is visible to everybody, even not-logged in users
    return true;
  }

  /** @see org.olat.search.service.indexer.Indexer#getSupportedTypeName() */
  public String getSupportedTypeName() {
    return OresHelper.calculateTypeName(ContextHelpModule.class);
  }

  /**
   * @see
   *     org.olat.search.service.indexer.AbstractHierarchicalIndexer#doIndex(org.olat.search.service.SearchResourceContext,
   *     java.lang.Object, org.olat.search.service.indexer.OlatFullIndexer)
   */
  @Override
  public void doIndex(
      SearchResourceContext parentResourceContext, Object parentObject, OlatFullIndexer indexWriter)
      throws IOException, InterruptedException {
    if (!ContextHelpModule.isContextHelpEnabled()) {
      // don't index context help when disabled
      return;
    }
    long startTime = System.currentTimeMillis();
    Set<String> helpPageIdentifyers = ContextHelpModule.getAllContextHelpPages();
    Set<String> languages = I18nModule.getEnabledLanguageKeys();
    if (log.isDebug())
      log.debug(
          "ContextHelpIndexer helpPageIdentifyers.size::"
              + helpPageIdentifyers.size()
              + " and languages.size::"
              + languages.size());
    // loop over all help pages
    for (String helpPageIdentifyer : helpPageIdentifyers) {
      String[] identifyerSplit = helpPageIdentifyer.split(":");
      String bundleName = identifyerSplit[0];
      String page = identifyerSplit[1];
      // fxdiff: FXOLAT-221: don't use velocity on images
      if (page == null || !page.endsWith(".html")) {
        continue;
      }

      // Translator with default locale. Locale is set to each language in the
      // language iteration below
      Translator pageTranslator = new PackageTranslator(bundleName, I18nModule.getDefaultLocale());
      // Open velocity page for this help page
      String pagePath = bundleName.replace('.', '/') + ContextHelpModule.CHELP_DIR + page;
      VelocityContainer container =
          new VelocityContainer("contextHelpPageVC", pagePath, pageTranslator, null);
      Context ctx = container.getContext();
      GlobalSettings globalSettings =
          new GlobalSettings() {
            public int getFontSize() {
              return 100;
            }

            public AJAXFlags getAjaxFlags() {
              return new EmptyAJAXFlags();
            }

            public ComponentRenderer getComponentRendererFor(Component source) {
              return null;
            }

            public boolean isIdDivsForced() {
              return false;
            }
          };
      Renderer renderer =
          Renderer.getInstance(
              container, pageTranslator, new EmptyURLBuilder(), null, globalSettings);
      // Add render decorator with helper methods
      VelocityRenderDecorator vrdec = new VelocityRenderDecorator(renderer, container, null);
      ctx.put("r", vrdec);
      // Add empty static dir url - only used to not generate error messages
      ctx.put("chelpStaticDirUrl", "");
      // Create document for each language using the velocity context
      for (String langCode : languages) {
        Locale locale = I18nManager.getInstance().getLocaleOrNull(langCode);
        String relPagePath = langCode + "/" + bundleName + "/" + page;
        if (log.isDebug()) log.debug("Indexing help page with path::" + relPagePath);
        SearchResourceContext searchResourceContext =
            new SearchResourceContext(parentResourceContext);
        searchResourceContext.setBusinessControlFor(
            OresHelper.createOLATResourceableType(
                ContextHelpModule.class.getSimpleName())); // to match the list of indexer
        // Create context help document and index now, set translator to current locale
        pageTranslator.setLocale(locale);
        Document document =
            ContextHelpDocument.createDocument(
                searchResourceContext, bundleName, page, pageTranslator, ctx, pagePath);
        indexWriter.addDocument(document);
      }
      IOUtils.closeQuietly(vrdec);
    }
    long indexTime = System.currentTimeMillis() - startTime;
    if (log.isDebug()) log.debug("ContextHelpIndexer finished in " + indexTime + " ms");
  }
}
/**
 * You can set a fix value for the cron expression, or let it calculated by the generator. The
 * generated value start at 2:00
 *
 * <p>Initial date: 15.08.2016<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
public class StatisticsCronGenerator implements FactoryBean<String> {

  private static final OLog log = Tracing.createLoggerFor(StatisticsCronGenerator.class);

  private int tomcatId;
  private String cronExpression;

  /**
   * [used by Spring]
   *
   * @param tomcatId
   */
  public void setTomcatId(int tomcatId) {
    this.tomcatId = tomcatId;
  }

  /**
   * [used by Spring]
   *
   * @param cronExpression
   */
  public void setCronExpression(String cronExpression) {
    if (CronExpression.isValidExpression(cronExpression)) {
      this.cronExpression = cronExpression;
    } else {
      if (StringHelper.containsNonWhitespace(cronExpression)) {
        // was not empty, so someone tried to set someting here, let user know that it was garbage
        log.warn(
            "Configured cron expression is not valid::"
                + cronExpression
                + " check your search.indexing.cronjob.expression property",
            null);
      }
      this.cronExpression = null;
    }
  }

  public String getCron() {
    if (cronExpression != null) {
      return cronExpression;
    }

    String cron = null;
    if (tomcatId >= 980) {
      int shift = 360 + ((tomcatId - 980) * 2);
      long hours = TimeUnit.MINUTES.toHours(shift);
      long remainMinute = shift - TimeUnit.HOURS.toMinutes(hours);
      cron = "0 " + remainMinute + " " + hours + " * * ? *";
    } else {
      int shift = 120 + (tomcatId * 2);
      long hours = TimeUnit.MINUTES.toHours(shift);
      long remainMinute = shift - TimeUnit.HOURS.toMinutes(hours);
      cron = "0 " + remainMinute + " " + hours + " * * ? *";
    }
    if (CronExpression.isValidExpression(cron)) {
      return cron;
    }
    return "0 10 5 * * ?";
  }

  @Override
  public String getObject() throws Exception {
    return getCron();
  }

  @Override
  public Class<?> getObjectType() {
    return String.class;
  }

  @Override
  public boolean isSingleton() {
    return true;
  }
}
Example #26
0
/** @author Mike Stock Comment: */
public class FileUtils {

  private static final OLog log = Tracing.createLoggerFor(FileUtils.class);

  private static int buffSize = 32 * 1024;

  // windows: invalid characters for filenames: \ / : * ? " < > |
  // linux: invalid characters for file/folder names: /, but you have to escape certain chars, like
  // ";$%&*"
  // OLAT reserved char: ":"
  private static char[] FILE_NAME_FORBIDDEN_CHARS = {
    '/', '\n', '\r', '\t', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'
  };
  // private static char[] FILE_NAME_ACCEPTED_CHARS = { 'ä', 'Ä', 'ü', 'Ü', 'ö', 'Ö', ' '};
  private static char[] FILE_NAME_ACCEPTED_CHARS = {
    '\u0228', '\u0196', '\u0252', '\u0220', '\u0246', '\u0214', ' '
  };

  /**
   * @param sourceFile
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean copyFileToDir(
      String sourceFile, String targetDir, FileFilter filter, String wt) {
    return copyFileToDir(new File(sourceFile), new File(targetDir), false, filter, wt);
  }

  /**
   * @param sourceFile
   * @param targetDir
   * @return true upon success
   */
  public static boolean copyFileToDir(String sourceFile, String targetDir) {
    return copyFileToDir(new File(sourceFile), new File(targetDir), false, null);
  }

  /**
   * @param sourceFile
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean moveFileToDir(
      String sourceFile, String targetDir, FileFilter filter, String wt) {
    return copyFileToDir(new File(sourceFile), new File(targetDir), true, filter, wt);
  }

  /**
   * @param sourceFile
   * @param targetDir
   * @return true upon success
   */
  public static boolean moveFileToDir(String sourceFile, String targetDir) {
    return copyFileToDir(new File(sourceFile), new File(targetDir), true, null);
  }

  /**
   * @param sourceFile
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean copyFileToDir(
      File sourceFile, File targetDir, FileFilter filter, String wt) {
    return copyFileToDir(sourceFile, targetDir, false, filter, wt);
  }

  /**
   * @param sourceFile
   * @param targetDir
   * @return true upon success
   */
  public static boolean copyFileToDir(File sourceFile, File targetDir, String wt) {
    return copyFileToDir(sourceFile, targetDir, false, null, wt);
  }

  /**
   * @param sourceFile
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean moveFileToDir(
      File sourceFile, File targetDir, FileFilter filter, String wt) {
    return copyFileToDir(sourceFile, targetDir, true, filter, wt);
  }

  /**
   * @param sourceFile
   * @param targetDir
   * @return true upon success
   */
  public static boolean moveFileToDir(File sourceFile, File targetDir) {
    return copyFileToDir(sourceFile, targetDir, true, null);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean copyDirToDir(
      String sourceDir, String targetDir, FileFilter filter, String wt) {
    return copyDirToDir(new File(sourceDir), new File(targetDir), false, filter, wt);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @return true upon success
   */
  public static boolean copyDirToDir(String sourceDir, String targetDir) {
    return copyDirToDir(new File(sourceDir), new File(targetDir), false, null);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean moveDirToDir(
      String sourceDir, String targetDir, FileFilter filter, String wt) {
    return moveDirToDir(new File(sourceDir), new File(targetDir), filter, wt);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @return true upon success
   */
  public static boolean moveDirToDir(String sourceDir, String targetDir, String wt) {
    return moveDirToDir(new File(sourceDir), new File(targetDir), wt);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean copyDirToDir(File sourceDir, File targetDir, FileFilter filter, String wt) {
    return copyDirToDir(sourceDir, targetDir, false, filter, wt);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @return true upon success
   */
  public static boolean copyDirToDir(File sourceDir, File targetDir, String wt) {
    return copyDirToDir(sourceDir, targetDir, false, null, wt);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @param filter file filter or NULL if no filter applied
   * @return true upon success
   */
  public static boolean moveDirToDir(File sourceDir, File targetDir, FileFilter filter, String wt) {
    return copyDirInternal(sourceDir, targetDir, true, false, filter, wt);
  }

  /**
   * @param sourceDir
   * @param targetDir
   * @return true upon success
   */
  public static boolean moveDirToDir(File sourceDir, File targetDir, String wt) {
    return copyDirInternal(sourceDir, targetDir, true, false, null, wt);
  }

  /**
   * Get the size in bytes of a directory
   *
   * @param path
   * @return true upon success
   */
  public static long getDirSize(File path) {
    Iterator path_iterator;
    File current_file;
    long size;

    File[] f = path.listFiles();
    if (f == null) {
      return 0;
    }
    path_iterator = (Arrays.asList(f)).iterator();
    size = 0;
    while (path_iterator.hasNext()) {
      current_file = (File) path_iterator.next();
      if (current_file.isFile()) {
        size += current_file.length();
      } else {
        size += getDirSize(current_file);
      }
    }
    return size;
  }

  /**
   * Copy the contents of a directory from one spot on hard disk to another. Will create any target
   * dirs if necessary.
   *
   * @param sourceDir directory which contents to copy on local hard disk.
   * @param targetDir new directory to be created on local hard disk.
   * @param filter file filter or NULL if no filter applied
   * @param move
   * @return true if the copy was successful.
   */
  public static boolean copyDirContentsToDir(
      File sourceDir, File targetDir, boolean move, FileFilter filter, String wt) {
    return copyDirInternal(sourceDir, targetDir, move, false, filter, wt);
  }

  /**
   * Copy the contents of a directory from one spot on hard disk to another. Will create any target
   * dirs if necessary.
   *
   * @param sourceDir directory which contents to copy on local hard disk.
   * @param targetDir new directory to be created on local hard disk.
   * @param move
   * @return true if the copy was successful.
   */
  public static boolean copyDirContentsToDir(
      File sourceDir, File targetDir, boolean move, String wt) {
    return copyDirInternal(sourceDir, targetDir, move, false, null, wt);
  }

  /**
   * Copy a directory from one spot on hard disk to another. Will create any target dirs if
   * necessary. The directory itself will be created on the target location.
   *
   * @param sourceDir directory to copy on local hard disk.
   * @param targetDir new directory to be created on local hard disk.
   * @param filter file filter or NULL if no filter applied
   * @param move
   * @return true if the copy was successful.
   */
  public static boolean copyDirToDir(
      File sourceDir, File targetDir, boolean move, FileFilter filter, String wt) {
    return copyDirInternal(sourceDir, targetDir, move, true, filter, wt);
  }

  /**
   * Copy a directory from one spot on hard disk to another. Will create any target dirs if
   * necessary. The directory itself will be created on the target location.
   *
   * @param sourceDir directory to copy on local hard disk.
   * @param targetDir new directory to be created on local hard disk.
   * @param move
   * @return true if the copy was successful.
   */
  public static boolean copyDirToDir(File sourceDir, File targetDir, boolean move, String wt) {
    return copyDirInternal(sourceDir, targetDir, move, true, null, wt);
  }

  /**
   * Copy a directory from one spot on hard disk to another. Will create any target dirs if
   * necessary.
   *
   * @param sourceDir directory to copy on local hard disk.
   * @param targetDir new directory to be created on local hard disk.
   * @param move
   * @param createDir If true, a directory with the name of the source directory will be created
   * @param filter file filter or NULL if no filter applied
   * @return true if the copy was successful.
   */
  private static boolean copyDirInternal(
      File sourceDir,
      File targetDir,
      boolean move,
      boolean createDir,
      FileFilter filter,
      String wt) {
    if (sourceDir.isFile()) return copyFileToDir(sourceDir, targetDir, move, filter, wt);
    if (!sourceDir.isDirectory()) return false;

    // copy only if filter allows. filtered items are considered a success
    // and not a failure of the operation
    if (filter != null && !filter.accept(sourceDir)) return true;

    targetDir.mkdirs(); // this will also copy/move empty directories
    if (!targetDir.isDirectory()) return false;

    if (createDir) targetDir = new File(targetDir, sourceDir.getName());
    if (move) {
      // in case of move just rename the directory to new location. The operation might fail
      // on a NFS or when copying accross different filesystems. In such cases, continue and copy
      // the files instead
      if (sourceDir.renameTo(targetDir)) return true;
    } // else copy structure

    targetDir.mkdirs();
    boolean success = true;
    String[] fileList = sourceDir.list();
    if (fileList == null) return false; // I/O error or not a directory
    for (int i = 0; i < fileList.length; i++) {
      File f = new File(sourceDir, fileList[i]);
      if (f.isDirectory()) {
        success &= copyDirToDir(f, targetDir, move, filter, wt + File.separator + f.getName());
      } else {
        success &= copyFileToDir(f, targetDir, move, filter, wt + " file=" + f.getName());
      }
    }

    // in case of a move accross different filesystems, clean up now
    if (move) {
      sourceDir.delete();
    }
    return success;
  }

  /**
   * Copy a file from one spot on hard disk to another. Will create any target dirs if necessary.
   *
   * @param sourceFile file to copy on local hard disk.
   * @param targetDir new file to be created on local hard disk.
   * @param move
   * @param filter file filter or NULL if no filter applied
   * @return true if the copy was successful.
   */
  public static boolean copyFileToDir(
      File sourceFile, File targetDir, boolean move, FileFilter filter, String wt) {
    try {
      // copy only if filter allows. filtered items are considered a success
      // and not a failure of the operation
      if (filter != null && !filter.accept(sourceFile)) return true;

      // catch if source is directory by accident
      if (sourceFile.isDirectory()) {
        return copyDirToDir(sourceFile, targetDir, move, filter, wt);
      }

      // create target directories
      targetDir.mkdirs(); // don't check for success... would return false on
      // existing dirs
      if (!targetDir.isDirectory()) return false;
      File targetFile = new File(targetDir, sourceFile.getName());

      // catch move/copy of "same" file -> buggy under Windows.
      if (sourceFile.getCanonicalPath().equals(targetFile.getCanonicalPath())) return true;
      if (move) {
        // try to rename it first - operation might only be successful on a local filesystem!
        if (sourceFile.renameTo(targetFile)) return true;
        // it failed, so continue with copy code!
      }

      bcopy(sourceFile, targetFile, "copyFileToDir:" + wt);

      if (move) {
        // to finish the move accross different filesystems we need to delete the source file
        sourceFile.delete();
      }
    } catch (IOException e) {
      log.error(
          "Could not copy file::"
              + sourceFile.getAbsolutePath()
              + " to dir::"
              + targetDir.getAbsolutePath(),
          e);
      return false;
    }
    return true;
  } // end copy

  /**
   * Copy method to copy a file to another file
   *
   * @param sourceFile
   * @param targetFile
   * @param move true: move file; false: copy file
   * @return true: success; false: failure
   */
  public static boolean copyFileToFile(File sourceFile, File targetFile, boolean move) {
    try {
      if (sourceFile.isDirectory() || targetFile.isDirectory()) {
        return false;
      }

      // create target directories
      targetFile.getParentFile().mkdirs(); // don't check for success... would return false on

      // catch move/copy of "same" file -> buggy under Windows.
      if (sourceFile.getCanonicalPath().equals(targetFile.getCanonicalPath())) return true;
      if (move) {
        // try to rename it first - operation might only be successful on a local filesystem!
        if (sourceFile.renameTo(targetFile)) return true;
        // it failed, so continue with copy code!
      }

      bcopy(sourceFile, targetFile, "copyFileToFile");

      if (move) {
        // to finish the move accross different filesystems we need to delete the source file
        sourceFile.delete();
      }
    } catch (IOException e) {
      log.error(
          "Could not copy file::"
              + sourceFile.getAbsolutePath()
              + " to file::"
              + targetFile.getAbsolutePath(),
          e);
      return false;
    }
    return true;
  } // end copy

  /**
   * Copy a file from one spot on hard disk to another. Will create any target dirs if necessary.
   *
   * @param sourceFile file to copy on local hard disk.
   * @param targetDir new file to be created on local hard disk.
   * @param move
   * @return true if the copy was successful.
   */
  public static boolean copyFileToDir(File sourceFile, File targetDir, boolean move, String wt) {
    return copyFileToDir(sourceFile, targetDir, move, null, wt);
  }

  /**
   * Copy an InputStream to an OutputStream.
   *
   * @param source InputStream, left open.
   * @param target OutputStream, left open.
   * @param length how many bytes to copy.
   * @return true if the copy was successful.
   */
  public static boolean copy(InputStream source, OutputStream target, long length) {
    if (length == 0) return true;
    try {
      int chunkSize = (int) Math.min(buffSize, length);
      long chunks = length / chunkSize;
      int lastChunkSize = (int) (length % chunkSize);
      // code will work even when chunkSize = 0 or chunks = 0;
      byte[] ba = new byte[chunkSize];

      for (long i = 0; i < chunks; i++) {
        int bytesRead = readBlocking(source, ba, 0, chunkSize);
        if (bytesRead != chunkSize) {
          throw new IOException();
        }
        target.write(ba);
      } // end for
      // R E A D / W R I T E last chunk, if any
      if (lastChunkSize > 0) {
        int bytesRead = readBlocking(source, ba, 0, lastChunkSize);
        if (bytesRead != lastChunkSize) {
          throw new IOException();
        }
        target.write(ba, 0, lastChunkSize);
      } // end if
    } catch (IOException e) {
      // don't log as error - happens all the time (ClientAbortException)
      if (log.isDebug())
        log.debug("Could not copy stream::" + e.getMessage() + " with length::" + length);
      return false;
    }
    return true;
  } // end copy

  /**
   * Copy an InputStream to an OutputStream, until EOF. Use only when you don't know the length.
   *
   * @param source InputStream, left open.
   * @param target OutputStream, left open.
   * @return true if the copy was successful.
   */
  @Deprecated
  public static boolean copy(InputStream source, OutputStream target) {
    try {

      int chunkSize = buffSize;
      // code will work even when chunkSize = 0 or chunks = 0;
      // Even for small files, we allocate a big buffer, since we
      // don't know the size ahead of time.
      byte[] ba = new byte[chunkSize];

      while (true) {
        int bytesRead = readBlocking(source, ba, 0, chunkSize);
        if (bytesRead > 0) {
          target.write(ba, 0, bytesRead);
        } else {
          break;
        } // hit eof
      } // end while
    } catch (MalformedStreamException e) {
      throw new OLATRuntimeException("Could not read stream", e);
    } catch (IOException e) {
      // don't log as error - happens all the time (ClientAbortException)
      if (log.isDebug()) log.debug("Could not copy stream::" + e.getMessage());
      return false;
    }
    return true;
  } // end copy

  /**
   * Reads exactly <code>len</code> bytes from the input stream into the byte array. This method
   * reads repeatedly from the underlying stream until all the bytes are read. InputStream.read is
   * often documented to block like this, but in actuality it does not always do so, and returns
   * early with just a few bytes. readBlockiyng blocks until all the bytes are read, the end of the
   * stream is detected, or an exception is thrown. You will always get as many bytes as you asked
   * for unless you get an eof or other exception. Unlike readFully, you find out how many bytes you
   * did get.
   *
   * @param in
   * @param b the buffer into which the data is read.
   * @param off the start offset of the data.
   * @param len the number of bytes to read.
   * @return number of bytes actually read.
   * @exception IOException if an I/O error occurs.
   */
  public static final int readBlocking(InputStream in, byte b[], int off, int len)
      throws IOException {
    int totalBytesRead = 0;

    while (totalBytesRead < len) {
      int bytesRead = in.read(b, off + totalBytesRead, len - totalBytesRead);
      if (bytesRead < 0) {
        break;
      }
      totalBytesRead += bytesRead;
    }
    return totalBytesRead;
  } // end readBlocking

  /**
   * Get rid of ALL files and subdirectories in given directory, and all subdirs under it,
   *
   * @param dir would normally be an existing directory, can be a file aswell
   * @param recursive true if you want subdirs deleted as well
   * @param deleteDir true if dir needs to be deleted as well
   * @return true upon success
   */
  public static boolean deleteDirsAndFiles(File dir, boolean recursive, boolean deleteDir) {

    boolean success = true;

    if (dir == null) return false;

    // We must empty child subdirs contents before can get rid of immediate
    // child subdirs
    if (recursive) {
      String[] allDirs = dir.list();
      if (allDirs != null) {
        for (int i = 0; i < allDirs.length; i++) {
          success &= deleteDirsAndFiles(new File(dir, allDirs[i]), true, false);
        }
      }
    }

    // delete all files in this dir
    String[] allFiles = dir.list();
    if (allFiles != null) {
      for (int i = 0; i < allFiles.length; i++) {
        File deleteFile = new File(dir, allFiles[i]);
        success &= deleteFile.delete();
      }
    }

    // delete passed dir
    if (deleteDir) {
      success &= dir.delete();
    }
    return success;
  } // end deleteDirContents

  /** @param newF */
  public static void createEmptyFile(File newF) {
    try {
      FileOutputStream fos = new FileOutputStream(newF);
      fos.close();
    } catch (IOException e) {
      throw new AssertException(
          "empty file could not be created for path " + newF.getAbsolutePath(), e);
    }
  }

  /**
   * @param baseDir
   * @param fileVisitor
   */
  public static void visitRecursively(File baseDir, FileVisitor fileVisitor) {
    visit(baseDir, fileVisitor);
  }

  private static void visit(File file, FileVisitor fileVisitor) {
    if (file.isDirectory()) {
      File[] files = file.listFiles();
      for (int i = 0; i < files.length; i++) {
        File f = files[i];
        visit(f, fileVisitor);
      }
    } else { // regular file
      fileVisitor.visit(file);
    }
  }

  /**
   * @param target
   * @param data
   * @param encoding
   */
  public static void save(File target, String data, String encoding) {
    try {
      save(new FileOutputStream(target), data, StringHelper.check4xMacRoman(encoding));
    } catch (IOException e) {
      throw new OLATRuntimeException(FileUtils.class, "could not save file", e);
    }
  }

  public static InputStream getInputStream(String data, String encoding) {
    try {
      byte[] ba = data.getBytes(StringHelper.check4xMacRoman(encoding));
      ByteArrayInputStream bis = new ByteArrayInputStream(ba);
      return bis;
    } catch (IOException e) {
      throw new OLATRuntimeException(FileUtils.class, "could not save to output stream", e);
    }
  }

  /**
   * @param target
   * @param data
   * @param encoding
   */
  public static void save(OutputStream target, String data, String encoding) {
    try {
      byte[] ba = data.getBytes(StringHelper.check4xMacRoman(encoding));
      ByteArrayInputStream bis = new ByteArrayInputStream(ba);
      bcopy(bis, target, "saveDataToFile");
    } catch (IOException e) {
      throw new OLATRuntimeException(FileUtils.class, "could not save to output stream", e);
    }
  }

  /**
   * Save a given input stream to a file
   *
   * @param source the input stream
   * @param target the file
   */
  public static void save(InputStream source, File target) {
    try {
      BufferedInputStream bis = new BufferedInputStream(source);
      BufferedOutputStream bos = getBos(target);

      cpio(bis, bos, "fileSave");

      bos.flush();
      bos.close();

      bis.close();
    } catch (IOException e) {
      throw new OLATRuntimeException(
          FileUtils.class, "could not save stream to file::" + target.getAbsolutePath(), e);
    }
  }

  public static String load(Resource source, String encoding) {
    try {
      return load(source.getInputStream(), encoding);
    } catch (FileNotFoundException e) {
      throw new RuntimeException("File not found: " + source.getFilename());
    } catch (IOException e) {
      throw new RuntimeException("File not found: " + source.getFilename());
    }
  }

  /**
   * @param source
   * @param encoding
   * @return the file in form of a string
   */
  public static String load(File source, String encoding) {
    try {
      return load(new FileInputStream(source), encoding);
    } catch (FileNotFoundException e) {
      throw new RuntimeException("could not copy file to ram: " + source.getAbsolutePath());
    }
  }

  /**
   * @param source
   * @param encoding
   * @return the inpustream in form of a string
   */
  public static String load(InputStream source, String encoding) {
    String htmltext = null;
    try {
      ByteArrayOutputStream bas = new ByteArrayOutputStream();
      boolean success = FileUtils.copy(source, bas);
      source.close();
      bas.close();
      if (!success) throw new RuntimeException("could not copy inputstream to ram");
      htmltext = bas.toString(StringHelper.check4xMacRoman(encoding));
    } catch (IOException e) {
      throw new OLATRuntimeException(FileUtils.class, "could not load from inputstream", e);
    }
    return htmltext;
  }

  /** @param is the inputstream to close, may also be null */
  public static void closeSafely(InputStream is) {
    if (is == null) return;
    try {
      is.close();
    } catch (IOException e) {
      // nothing to do
    }
  }

  /** @param os the outputstream to close, may also be null */
  public static void closeSafely(OutputStream os) {
    if (os == null) return;
    try {
      os.close();
    } catch (IOException e) {
      // nothing to do
    }
  }

  /**
   * Extract file suffix. E.g. 'html' from index.html
   *
   * @param filePath
   * @return return empty String "" without suffix.
   */
  public static String getFileSuffix(String filePath) {
    int lastDot = filePath.lastIndexOf('.');
    if (lastDot > 0) {
      if (lastDot < filePath.length()) return filePath.substring(lastDot + 1).toLowerCase();
    }
    return "";
  }

  /**
   * Simple check for filename validity. It compares each character if it is accepted, forbidden or
   * in a certain (Latin-1) range.
   *
   * <p>Characters < 33 --> control characters and space Characters > 255 --> above ASCII
   * http://www.danshort.com/ASCIImap/ TODO: control chars from 127 - 157 should also not be
   * accepted TODO: how about non ascii chars in filenames, they should also work! See: OLAT-5704
   *
   * @param filename
   * @return true if filename valid
   */
  public static boolean validateFilename(String filename) {
    if (filename == null) {
      return false;
    }
    Arrays.sort(FILE_NAME_FORBIDDEN_CHARS);
    Arrays.sort(FILE_NAME_ACCEPTED_CHARS);

    for (int i = 0; i < filename.length(); i++) {
      char character = filename.charAt(i);
      if (Arrays.binarySearch(FILE_NAME_ACCEPTED_CHARS, character) >= 0) {
        continue;
      } else if (character < 33
          || character > 255
          || Arrays.binarySearch(FILE_NAME_FORBIDDEN_CHARS, character) >= 0) {
        return false;
      }
    }
    // check if there are any unwanted path denominators in the name
    if (filename.indexOf("..") > -1) {
      return false;
    }
    return true;
  }

  /**
   * Creates a new directory in the specified directory, using the given prefix and suffix strings
   * to generate its name. It uses File.createTempFile() and should provide a unique name.
   *
   * @param prefix
   * @param suffix
   * @param directory
   * @return
   */
  public static File createTempDir(String prefix, String suffix, File directory) {
    File tmpDir = null;
    try {
      File tmpFile = File.createTempFile(prefix, suffix, directory);
      if (tmpFile.exists()) {
        tmpFile.delete();
      }
      boolean tmpDirCreated = tmpFile.mkdir();
      if (tmpDirCreated) {
        tmpDir = tmpFile;
      }
    } catch (Exception e) {
      // bummer!
    }
    return tmpDir;
  }

  // the following is for cleaning up file I/O stuff ... so it works fine on NFS

  public static final int BSIZE = 8 * 1024;

  public static void bcopy(File src, File dst, String wt)
      throws FileNotFoundException, IOException {
    bcopy(new FileInputStream(src), new FileOutputStream(dst), wt);
  }

  public static void bcopy(InputStream src, File dst, String wt)
      throws FileNotFoundException, IOException {
    bcopy(src, new FileOutputStream(dst), "copyStreamToFile:" + wt);
  }

  public static BufferedOutputStream getBos(FileOutputStream of) {
    return new BufferedOutputStream(of, BSIZE);
  }

  public static BufferedOutputStream getBos(OutputStream os) {
    return new BufferedOutputStream(os, BSIZE);
  }

  public static BufferedOutputStream getBos(File of) throws FileNotFoundException {
    return getBos(new FileOutputStream(of));
  }

  public static BufferedOutputStream getBos(String fname) throws FileNotFoundException {
    return getBos(new File(fname));
  }

  /**
   * Buffered copy streams (closes both streams when done)
   *
   * @param src InputStream
   * @param dst OutputStream
   * @throws IOException
   */
  public static void bcopy(InputStream src, OutputStream dst, String wt) throws IOException {

    BufferedInputStream bis = new BufferedInputStream(src);
    BufferedOutputStream bos = getBos(dst);

    try {
      cpio(bis, bos, wt);
      bos.flush();
    } catch (IOException e) {
      throw new RuntimeException("I/O error in cpio " + wt);
    } finally {
      bos.close();
      dst.close();
      bis.close(); // no effect
      src.close(); // no effect
    }
  }

  /**
   * copy in, copy out (leaves both streams open)
   *
   * <p>
   *
   * @see FileUtils.getBos() which creates a matching BufferedOutputStream
   * @param in BuferedInputStream
   * @param out BufferedOutputStream
   * @param wt What this I/O is about
   */
  public static long cpio(BufferedInputStream in, BufferedOutputStream out, String wt)
      throws IOException {

    byte[] buffer = new byte[BSIZE];

    int c;
    long tot = 0;
    long s = System.nanoTime();
    while ((c = in.read(buffer, 0, buffer.length)) != -1) {
      out.write(buffer, 0, c);
      tot += c;
    }

    long tim = System.nanoTime() - s;
    double dtim =
        tim == 0 ? 0.5 : tim; // avg of those less than 1 nanoseconds is taken as 0.5 nanoseconds
    double bps = tot * 1000 * 1000 / dtim;
    log.debug(
        String.format(
            "cpio %,13d bytes %6.2f ms avg %6.1f Mbps %s%n",
            tot, dtim / 1000 / 1000, bps / 1024, wt));
    return tot;
  }
}
/**
 * Description: Useful functions for download
 *
 * @author Alexander Schneider
 */
public class QTIResultManager implements UserDataDeletable {

  private static final OLog log = Tracing.createLoggerFor(QTIResultManager.class);

  private static QTIResultManager instance;

  private DB dbInstance;

  /** Constructor for QTIResultManager. */
  private QTIResultManager() {
    instance = this;
  }

  /** @return QTIResultManager */
  public static QTIResultManager getInstance() {
    return instance;
  }

  /**
   * [user by Spring]
   *
   * @param dbInstance
   */
  public void setDbInstance(DB dbInstance) {
    this.dbInstance = dbInstance;
  }

  /**
   * @param olatResource
   * @param olatResourceDetail
   * @param repositoryRef
   * @return True if true, false otherwise.
   */
  public boolean hasResultSets(Long olatResource, String olatResourceDetail, Long repositoryRef) {
    StringBuilder sb = new StringBuilder();
    sb.append("select count(rset.key) from ")
        .append(QTIResultSet.class.getName())
        .append(" as rset ")
        .append(
            "where rset.olatResource=:resId and rset.olatResourceDetail=:resSubPath and rset.repositoryRef=:repoKey");

    Number count =
        dbInstance
            .getCurrentEntityManager()
            .createQuery(sb.toString(), Number.class)
            .setParameter("resId", olatResource)
            .setParameter("resSubPath", olatResourceDetail)
            .setParameter("repoKey", repositoryRef)
            .getSingleResult();
    return count == null ? false : count.intValue() > 0;
  }

  /**
   * Get the resulkt sets.
   *
   * @param olatResource
   * @param olatResourceDetail
   * @param repositoryRef
   * @param identity May be null
   * @return List of resultsets
   */
  public List<QTIResultSet> getResultSets(
      Long olatResource, String olatResourceDetail, Long repositoryRef, Identity identity) {
    StringBuilder sb = new StringBuilder();
    sb.append("select rset from ")
        .append(QTIResultSet.class.getName())
        .append(" as rset ")
        .append(
            "where rset.olatResource=:resId and rset.olatResourceDetail=:resSubPath and rset.repositoryRef=:repoKey");
    if (identity != null) {
      sb.append(" and rset.identity.key=:identityKey ");
    }

    TypedQuery<QTIResultSet> query =
        dbInstance
            .getCurrentEntityManager()
            .createQuery(sb.toString(), QTIResultSet.class)
            .setParameter("resId", olatResource)
            .setParameter("resSubPath", olatResourceDetail)
            .setParameter("repoKey", repositoryRef);
    if (identity != null) {
      query.setParameter("identityKey", identity.getKey());
    }
    return query.getResultList();
  }

  /**
   * selects all resultsets of a IQCourseNode of a particular course
   *
   * @param olatResource
   * @param olatResourceDetail
   * @param repositoryRef
   * @return List of QTIResult objects
   */
  public List<QTIResult> selectResults(
      Long olatResource,
      String olatResourceDetail,
      Long repositoryRef,
      List<SecurityGroup> limitToSecGroups,
      int type) {
    StringBuilder sb = new StringBuilder();
    sb.append("select res from ")
        .append(QTIResult.class.getName())
        .append(" as res ")
        .append(" inner join res.resultSet as rset")
        .append(" inner join rset.identity as ident")
        .append(" inner join ident.user as usr")
        .append(
            " where rset.olatResource=:resId and rset.olatResourceDetail=:resSubPath and rset.repositoryRef=:repoKey");
    if (limitToSecGroups != null && limitToSecGroups.size() > 0) {
      sb.append(" and rset.identity.key in ( select secMembership.identity.key from ")
          .append(SecurityGroupMembershipImpl.class.getName())
          .append(" secMembership ")
          .append("   where secMembership.securityGroup in (:secGroups)")
          .append(" )");
    }

    if (type == 1 || type == 2) {
      // 1 -> iqtest, 2 -> iqself
      sb.append(" order by usr.properties['")
          .append(UserConstants.LASTNAME)
          .append("'] , rset.assessmentID, res.itemIdent");
    } else {
      // 3 -> iqsurv: the alphabetical assortment above could destroy the anonymization
      // if names and quantity of the persons is well-known
      sb.append(" order by rset.creationDate, rset.assessmentID, res.itemIdent");
    }

    TypedQuery<QTIResult> query =
        dbInstance
            .getCurrentEntityManager()
            .createQuery(sb.toString(), QTIResult.class)
            .setParameter("resId", olatResource)
            .setParameter("resSubPath", olatResourceDetail)
            .setParameter("repoKey", repositoryRef);

    if (limitToSecGroups != null && limitToSecGroups.size() > 0) {
      query.setParameter("secGroups", limitToSecGroups);
    }

    return query.getResultList();
  }

  /**
   * Same as above but only count the number of results
   *
   * @param olatResource
   * @param olatResourceDetail
   * @param repositoryRef
   * @return
   */
  public int countResults(Long olatResource, String olatResourceDetail, Long repositoryRef) {
    StringBuilder sb = new StringBuilder();
    sb.append("select count(res.key) from ")
        .append(QTIResult.class.getName())
        .append(" as res ")
        .append(" inner join res.resultSet as rset")
        .append(" inner join rset.identity as ident")
        .append(" inner join ident.user as usr")
        .append(
            " where rset.olatResource=:resId and rset.olatResourceDetail=:resSubPath and rset.repositoryRef=:repoKey");

    Number count =
        dbInstance
            .getCurrentEntityManager()
            .createQuery(sb.toString(), Number.class)
            .setParameter("resId", olatResource)
            .setParameter("resSubPath", olatResourceDetail)
            .setParameter("repoKey", repositoryRef)
            .getSingleResult();
    return count == null ? 0 : count.intValue();
  }

  /**
   * Deletes all Results and ResultSets of a test, selftest or survey
   *
   * @param olatRes
   * @param olatResDet
   * @param repRef
   * @return deleted ResultSets
   */
  public int deleteAllResults(Long olatRes, String olatResDet, Long repRef) {
    StringBuilder sb = new StringBuilder();
    sb.append("select rset from ").append(QTIResultSet.class.getName()).append(" as rset ");
    sb.append(
        " where rset.olatResource=:resId and rset.olatResourceDetail=:resSubPath and rset.repositoryRef=:repoKey ");

    EntityManager em = dbInstance.getCurrentEntityManager();
    List<QTIResultSet> sets =
        em.createQuery(sb.toString(), QTIResultSet.class)
            .setParameter("resId", olatRes)
            .setParameter("resSubPath", olatResDet)
            .setParameter("repoKey", repRef)
            .getResultList();

    StringBuilder delSb = new StringBuilder();
    delSb
        .append("delete from ")
        .append(QTIResult.class.getName())
        .append(" as res where res.resultSet.key=:setKey");
    Query delResults = em.createQuery(delSb.toString());
    for (QTIResultSet set : sets) {
      delResults.setParameter("setKey", set.getKey()).executeUpdate();
      em.remove(set);
    }
    return sets.size();
  }

  /**
   * Deletes all Results AND all ResultSets for certain QTI-ResultSet.
   *
   * @param qtiResultSet
   */
  public void deleteResults(QTIResultSet qtiResultSet) {
    deleteAllResults(
        qtiResultSet.getOlatResource(),
        qtiResultSet.getOlatResourceDetail(),
        qtiResultSet.getRepositoryRef());
  }

  /**
   * translates the answerstring stored in table o_qtiresult
   *
   * @param answerCode
   * @return translation
   */
  public static Map<String, String> parseResponseStrAnswers(String answerCode) {
    // calculate the correct answer, if eventually needed
    int modus = 0;
    int startIdentPosition = 0;
    int startCharacterPosition = 0;
    String tempIdent = null;
    Map<String, String> result = new HashMap<String, String>();
    char c;

    for (int i = 0; i < answerCode.length(); i++) {
      c = answerCode.charAt(i);
      if (modus == 0) {
        if (c == '[') {
          String sIdent = answerCode.substring(startIdentPosition, i);
          if (sIdent.length() > 0) {
            tempIdent = sIdent;
            modus = 1;
          }
        }
      } else if (modus == 1) {
        if (c == '[') {
          startCharacterPosition = i + 1;
          modus = 2;
        } else if (c == ']') {
          startIdentPosition = i + 1;
          tempIdent = null;
          modus = 0;
        }
      } else if (modus == 2) {
        if (c == ']') {
          if (answerCode.charAt(i - 1) != '\\') {
            String s = answerCode.substring(startCharacterPosition, i);
            if (tempIdent != null) result.put(tempIdent, s.replaceAll("\\\\\\]", "]"));
            modus = 1;
          }
        }
      }
    }
    return result;
  }

  /**
   * translates the answerstring stored in table o_qtiresult
   *
   * @param answerCode
   * @return translation
   */
  public static List<String> parseResponseLidAnswers(String answerCode) {
    // calculate the correct answer, if eventually needed
    int modus = 0;
    int startCharacterPosition = 0;
    List<String> result = new ArrayList<String>();
    char c;

    for (int i = 0; i < answerCode.length(); i++) {
      c = answerCode.charAt(i);
      if (modus == 0) {
        if (c == '[') {
          modus = 1;
        }
      } else if (modus == 1) {
        if (c == '[') {
          startCharacterPosition = i + 1;
          modus = 2;
        } else if (c == ']') {
          modus = 0;
        }
      } else if (modus == 2) {
        if (c == ']') {
          if (answerCode.charAt(i - 1) != '\\') {
            String s = answerCode.substring(startCharacterPosition, i);
            result.add(s.replaceAll("\\\\\\]", "]"));
            modus = 1;
          }
        }
      }
    }
    return result;
  }

  /**
   * Find all ResultSets for certain identity.
   *
   * @param identity
   * @param assessmentID
   * @return
   */
  public List<QTIResultSet> findQtiResultSets(Identity identity) {
    StringBuilder sb = new StringBuilder();
    sb.append("select rset from ")
        .append(QTIResultSet.class.getName())
        .append(" as rset")
        .append(" where rset.identity.key=:identityKey");
    return dbInstance
        .getCurrentEntityManager()
        .createQuery(sb.toString(), QTIResultSet.class)
        .setParameter("identityKey", identity.getKey())
        .getResultList();
  }

  /**
   * Delete all ResultSet for certain identity.
   *
   * @param identity
   */
  public void deleteUserData(Identity identity, String newDeletedUserName) {
    List<QTIResultSet> qtiResults = findQtiResultSets(identity);
    for (QTIResultSet set : qtiResults) {
      deleteResultSet(set);
    }
    if (log.isDebug()) {
      log.debug("Delete all QTI result data in db for identity=" + identity);
    }
  }

  /**
   * Delete all qti-results and qti-result-set entry for certain result-set.
   *
   * @param rSet
   */
  public void deleteResultSet(QTIResultSet rSet) {
    EntityManager em = dbInstance.getCurrentEntityManager();

    StringBuilder delResultsSb = new StringBuilder();
    delResultsSb
        .append("delete from ")
        .append(QTIResult.class.getName())
        .append(" as res where res.resultSet.key=:setKey");
    em.createQuery(delResultsSb.toString()).setParameter("setKey", rSet.getKey()).executeUpdate();

    StringBuilder delSetSb = new StringBuilder();
    delSetSb
        .append("delete from ")
        .append(QTIResultSet.class.getName())
        .append(" as rset where rset.key=:setKey");
    em.createQuery(delSetSb.toString()).setParameter("setKey", rSet.getKey()).executeUpdate();
  }
}
Example #28
0
/**
 * The task which execute the bulk assessment<br>
 * Initial date: 20.11.2013<br>
 *
 * @author srosse, [email protected], http://www.frentix.com
 */
public class BulkAssessmentTask implements LongRunnable, TaskAwareRunnable, Sequential {

  private static final long serialVersionUID = 4614724183354689151L;
  private static final OLog log = Tracing.createLoggerFor(BulkAssessmentTask.class);

  private OLATResourceable courseRes;
  private String courseNodeIdent;
  private BulkAssessmentDatas datas;
  private BulkAssessmentSettings settings;
  private Long coachedIdentity;

  private transient Task task;
  private transient File unzipped;

  public BulkAssessmentTask(
      OLATResourceable courseRes,
      AssessableCourseNode courseNode,
      BulkAssessmentDatas datas,
      Long coachedIdentity) {
    this.courseRes = OresHelper.clone(courseRes);
    this.courseNodeIdent = courseNode.getIdent();
    this.settings = new BulkAssessmentSettings(courseNode);
    this.datas = datas;
    this.coachedIdentity = coachedIdentity;
  }

  public String getCourseNodeIdent() {
    return courseNodeIdent;
  }

  public BulkAssessmentSettings getSettings() {
    return settings;
  }

  public BulkAssessmentDatas getDatas() {
    return datas;
  }

  @Override
  public void setTask(Task task) {
    this.task = task;
  }

  /** Used by to task executor, without any GUI */
  @Override
  public void run() {
    final List<BulkAssessmentFeedback> feedbacks = new ArrayList<>();
    try {
      log.audit("Start process bulk assessment");

      LoggingResourceable[] infos = new LoggingResourceable[2];
      if (task != null && task.getCreator() != null) {
        UserSession session = new UserSession();
        session.setIdentity(task.getCreator());
        session.setSessionInfo(
            new SessionInfo(task.getCreator().getKey(), task.getCreator().getName()));
        ThreadLocalUserActivityLoggerInstaller.initUserActivityLogger(session);
        infos[0] = LoggingResourceable.wrap(courseRes, OlatResourceableType.course);
        ThreadLocalUserActivityLogger.addLoggingResourceInfo(infos[0]);
        infos[1] = LoggingResourceable.wrap(getCourseNode());
        ThreadLocalUserActivityLogger.addLoggingResourceInfo(infos[1]);
      }

      doProcess(feedbacks);
      log.audit("End process bulk assessment");
      cleanup();

      ThreadLocalUserActivityLogger.log(AssessmentLoggingAction.ASSESSMENT_BULK, getClass(), infos);
    } catch (Exception e) {
      log.error("", e);
      feedbacks.add(new BulkAssessmentFeedback("", "bulk.assessment.error"));
      throw e;
    } finally {
      cleanupUnzip();
      sendFeedback(feedbacks);
    }
  }

  public List<BulkAssessmentFeedback> process() {
    List<BulkAssessmentFeedback> feedbacks = new ArrayList<>();
    try {
      LoggingResourceable infos = LoggingResourceable.wrap(getCourseNode());
      ThreadLocalUserActivityLogger.addLoggingResourceInfo(infos);

      doProcess(feedbacks);
      cleanup();
    } catch (Exception e) {
      log.error("", e);
      feedbacks.add(new BulkAssessmentFeedback("", "bulk.assessment.error"));
    } finally {
      cleanupUnzip();
    }
    return feedbacks;
  }

  private void cleanup() {
    if (StringHelper.containsNonWhitespace(datas.getDataBackupFile())) {
      OlatRootFileImpl backupFile = new OlatRootFileImpl(datas.getDataBackupFile(), null);
      if (backupFile.exists()) {
        File dir = backupFile.getBasefile().getParentFile();
        if (dir != null && dir.exists()) {
          FileUtils.deleteDirsAndFiles(dir, true, true);
        }
      }
    }
    cleanupUnzip();
  }

  private void cleanupUnzip() {
    try {
      if (unzipped != null && unzipped.exists()) {
        FileUtils.deleteDirsAndFiles(unzipped, true, true);
      }
    } catch (Exception e) {
      log.error("Cannot cleanup unzipped datas after bulk assessment", e);
    }
  }

  private void sendFeedback(List<BulkAssessmentFeedback> feedbacks) {
    if (task == null) {
      log.error("Haven't a task to know creator and modifiers of the task", null);
      return;
    }

    Identity creator = task.getCreator();
    String language = creator.getUser().getPreferences().getLanguage();
    Locale locale = I18nManager.getInstance().getLocaleOrDefault(language);
    Translator translator =
        Util.createPackageTranslator(
            BulkAssessmentOverviewController.class,
            locale,
            Util.createPackageTranslator(AssessmentManager.class, locale));
    MailManager mailManager = CoreSpringFactory.getImpl(MailManager.class);
    TaskExecutorManager taskManager = CoreSpringFactory.getImpl(TaskExecutorManager.class);

    String feedbackStr = renderFeedback(feedbacks, translator);

    MailBundle mail = new MailBundle();
    mail.setToId(creator);
    mail.setFrom(WebappHelper.getMailConfig("mailReplyTo"));
    List<Identity> modifiers = taskManager.getModifiers(task);
    if (modifiers.size() > 0) {
      ContactList cc = new ContactList("CC");
      cc.addAllIdentites(modifiers);
      mail.setContactList(cc);
    }

    String businessPath = "";
    ICourse course = CourseFactory.loadCourse(courseRes);
    CourseNode node = course.getRunStructure().getNode(courseNodeIdent);
    String courseTitle = course.getCourseTitle();
    String nodeTitle = node.getShortTitle();
    String numOfAssessedIds = Integer.toString(datas == null ? 0 : datas.getRowsSize());
    String date = Formatter.getInstance(locale).formatDateAndTime(new Date());

    mail.setContext(new MailContextImpl(courseRes, courseNodeIdent, businessPath));
    String subject =
        translator.translate("confirmation.mail.subject", new String[] {courseTitle, nodeTitle});
    String body =
        translator.translate(
            "confirmation.mail.body",
            new String[] {courseTitle, nodeTitle, feedbackStr, numOfAssessedIds, date});
    mail.setContent(subject, body);
    mailManager.sendMessage(mail);
  }

  public static String renderFeedback(
      List<BulkAssessmentFeedback> feedbacks, Translator translator) {
    UserManager userManager = CoreSpringFactory.getImpl(UserManager.class);

    StringBuilder sb = new StringBuilder();
    for (BulkAssessmentFeedback feedback : feedbacks) {
      String errorKey = feedback.getErrorKey();
      String msg = translator.translate(errorKey);
      String assessedName;
      if (feedback.getAssessedIdentity() != null) {
        assessedName = userManager.getUserDisplayName(feedback.getAssessedIdentity());
      } else {
        assessedName = feedback.getAssessedId();
      }
      sb.append(assessedName).append(": ").append(msg).append("\n");
    }
    return sb.toString();
  }

  public static boolean isBulkAssessable(CourseNode courseNode) {
    boolean bulkAssessability = false;
    if (courseNode instanceof MSCourseNode
        || courseNode instanceof TACourseNode
        || courseNode instanceof GTACourseNode
        || courseNode instanceof ProjectBrokerCourseNode) {
      // now a more fine granular check on bulk features. only show wizard for nodes that have at
      // least one
      BulkAssessmentSettings settings =
          new BulkAssessmentSettings((AssessableCourseNode) courseNode);
      if (settings.isHasPassed()
          || settings.isHasScore()
          || settings.isHasUserComment()
          || settings.isHasReturnFiles()) {
        bulkAssessability = true;
      }
    }
    return bulkAssessability;
  }

  private AssessableCourseNode getCourseNode() {
    ICourse course = CourseFactory.loadCourse(courseRes);
    CourseNode node = course.getRunStructure().getNode(courseNodeIdent);
    if (node instanceof AssessableCourseNode) {
      return (AssessableCourseNode) node;
    }
    return null;
  }

  private void doProcess(List<BulkAssessmentFeedback> feedbacks) {
    final DB dbInstance = DBFactory.getInstance();
    final BaseSecurity securityManager = CoreSpringFactory.getImpl(BaseSecurity.class);
    final Identity coachIdentity = securityManager.loadIdentityByKey(coachedIdentity);
    final ICourse course = CourseFactory.loadCourse(courseRes);
    final AssessableCourseNode courseNode = getCourseNode();
    final Roles studentRoles = new Roles(false, false, false, false, false, false, false, false);

    final boolean hasUserComment = courseNode.hasCommentConfigured();
    final boolean hasScore = courseNode.hasScoreConfigured();
    final boolean hasPassed = courseNode.hasPassedConfigured();
    final boolean hasReturnFiles =
        (StringHelper.containsNonWhitespace(datas.getReturnFiles())
            && (courseNode instanceof TACourseNode || courseNode instanceof GTACourseNode));

    if (hasReturnFiles) {
      try {
        OlatRootFileImpl returnFilesZipped = new OlatRootFileImpl(datas.getReturnFiles(), null);
        String tmp = FolderConfig.getCanonicalTmpDir();
        unzipped = new File(tmp, UUID.randomUUID().toString() + File.separatorChar);
        unzipped.mkdirs();
        ZipUtil.unzip(returnFilesZipped.getBasefile(), unzipped);
      } catch (Exception e) {
        log.error("Cannot unzip the return files during bulk assessment", e);
      }
    }

    Float min = null;
    Float max = null;
    Float cut = null;
    if (hasScore) {
      min = courseNode.getMinScoreConfiguration();
      max = courseNode.getMaxScoreConfiguration();
    }
    if (hasPassed) {
      cut = courseNode.getCutValueConfiguration();
    }

    int count = 0;
    List<BulkAssessmentRow> rows = datas.getRows();
    for (BulkAssessmentRow row : rows) {
      Long identityKey = row.getIdentityKey();
      if (identityKey == null) {
        feedbacks.add(new BulkAssessmentFeedback("bulk.action.no.such.user", row.getAssessedId()));
        continue; // nothing to do
      }

      Identity identity = securityManager.loadIdentityByKey(identityKey);
      IdentityEnvironment ienv = new IdentityEnvironment(identity, studentRoles);
      UserCourseEnvironment uce =
          new UserCourseEnvironmentImpl(ienv, course.getCourseEnvironment());

      // update comment, empty string will reset comment
      String userComment = row.getComment();
      if (hasUserComment && userComment != null) {
        // Update userComment in db
        courseNode.updateUserUserComment(userComment, uce, coachIdentity);
        // LD: why do we have to update the efficiency statement?
        // EfficiencyStatementManager esm =	EfficiencyStatementManager.getInstance();
        // esm.updateUserEfficiencyStatement(uce);
      }

      // update score
      Float score = row.getScore();
      if (hasScore && score != null) {
        // score < minimum score
        if ((min != null && score.floatValue() < min.floatValue())
            || (score.floatValue() < AssessmentHelper.MIN_SCORE_SUPPORTED)) {
          // "bulk.action.lessThanMin";
        }
        // score > maximum score
        else if ((max != null && score.floatValue() > max.floatValue())
            || (score.floatValue() > AssessmentHelper.MAX_SCORE_SUPPORTED)) {
          // "bulk.action.greaterThanMax";
        } else {
          // score between minimum and maximum score
          ScoreEvaluation se;
          if (hasPassed && cut != null) {
            Boolean passed =
                (score.floatValue() >= cut.floatValue()) ? Boolean.TRUE : Boolean.FALSE;
            se = new ScoreEvaluation(score, passed);
          } else {
            se = new ScoreEvaluation(score, null);
          }

          // Update score,passed properties in db, and the user's efficiency statement
          courseNode.updateUserScoreEvaluation(se, uce, coachIdentity, false);
        }
      }

      Boolean passed = row.getPassed();
      if (hasPassed
          && passed != null
          && cut
              == null) { // Configuration of manual assessment --> Display passed/not passed: yes,
        // Type of display: Manual by tutor
        ScoreEvaluation seOld = courseNode.getUserScoreEvaluation(uce);
        Float oldScore = seOld.getScore();
        ScoreEvaluation se = new ScoreEvaluation(oldScore, passed);
        // Update score,passed properties in db, and the user's efficiency statement
        boolean incrementAttempts = false;
        courseNode.updateUserScoreEvaluation(se, uce, coachIdentity, incrementAttempts);
      }

      boolean identityHasReturnFile = false;
      if (hasReturnFiles && row.getReturnFiles() != null && row.getReturnFiles().size() > 0) {
        String assessedId = row.getAssessedId();
        File assessedFolder = new File(unzipped, assessedId);
        identityHasReturnFile = assessedFolder.exists();
        if (identityHasReturnFile) {
          processReturnFile(courseNode, row, uce, assessedFolder);
        }
      }

      if (courseNode instanceof GTACourseNode) {
        // push the state further
        GTACourseNode gtaNode = (GTACourseNode) courseNode;
        if ((hasScore && score != null) || (hasPassed && passed != null)) {
          // pushed to graded
          updateTasksState(gtaNode, uce, TaskProcess.grading);
        } else if (hasReturnFiles) {
          // push to revised
          updateTasksState(gtaNode, uce, TaskProcess.correction);
        }
      }

      if (count++ % 5 == 0) {
        dbInstance.commitAndCloseSession();
      } else {
        dbInstance.commit();
      }
    }
  }

  private void updateTasksState(
      GTACourseNode courseNode, UserCourseEnvironment uce, TaskProcess status) {
    final GTAManager gtaManager = CoreSpringFactory.getImpl(GTAManager.class);
    Identity identity = uce.getIdentityEnvironment().getIdentity();
    RepositoryEntry entry = uce.getCourseEnvironment().getCourseGroupManager().getCourseEntry();

    org.olat.course.nodes.gta.Task gtaTask;
    TaskList taskList = gtaManager.getTaskList(entry, courseNode);
    if (taskList == null) {
      taskList = gtaManager.createIfNotExists(entry, courseNode);
      gtaTask = gtaManager.createTask(null, taskList, status, null, identity, courseNode);
    } else {
      gtaTask = gtaManager.getTask(identity, taskList);
      if (gtaTask == null) {
        gtaManager.createTask(null, taskList, status, null, identity, courseNode);
      }
    }

    gtaManager.nextStep(status, courseNode);
  }

  private void processReturnFile(
      AssessableCourseNode courseNode,
      BulkAssessmentRow row,
      UserCourseEnvironment uce,
      File assessedFolder) {
    String assessedId = row.getAssessedId();
    Identity identity = uce.getIdentityEnvironment().getIdentity();
    VFSContainer returnBox = getReturnBox(uce, courseNode, identity);
    if (returnBox != null) {
      for (String returnFilename : row.getReturnFiles()) {
        File returnFile = new File(assessedFolder, returnFilename);
        VFSItem currentReturnLeaf = returnBox.resolve(returnFilename);
        if (currentReturnLeaf != null) {
          // remove the current file (delete make a version if it is enabled)
          currentReturnLeaf.delete();
        }

        VFSLeaf returnLeaf = returnBox.createChildLeaf(returnFilename);
        if (returnFile.exists()) {
          try {
            InputStream inStream = new FileInputStream(returnFile);
            VFSManager.copyContent(inStream, returnLeaf);
          } catch (FileNotFoundException e) {
            log.error("Cannot copy return file " + returnFilename + " from " + assessedId, e);
          }
        }
      }
    }
  }

  /**
   * Return the target folder of the assessed identity. This is a factory method which take care of
   * the type of the course node.
   *
   * @param uce
   * @param courseNode
   * @param identity
   * @return
   */
  private VFSContainer getReturnBox(
      UserCourseEnvironment uce, CourseNode courseNode, Identity identity) {
    VFSContainer returnContainer = null;
    if (courseNode instanceof GTACourseNode) {
      final GTAManager gtaManager = CoreSpringFactory.getImpl(GTAManager.class);
      CourseEnvironment courseEnv = uce.getCourseEnvironment();
      returnContainer =
          gtaManager.getCorrectionContainer(courseEnv, (GTACourseNode) courseNode, identity);
    } else {
      String returnPath =
          ReturnboxController.getReturnboxPathRelToFolderRoot(
              uce.getCourseEnvironment(), courseNode);
      OlatRootFolderImpl rootFolder = new OlatRootFolderImpl(returnPath, null);
      VFSItem assessedItem = rootFolder.resolve(identity.getName());
      if (assessedItem == null) {
        returnContainer = rootFolder.createChildContainer(identity.getName());
      } else if (assessedItem instanceof VFSContainer) {
        returnContainer = (VFSContainer) assessedItem;
      }
    }
    return returnContainer;
  }
}
/**
 * Converter class to take a LoggingObject and convert it into a (csv) line
 *
 * <p>Primarily a helper class for the ICourseLogExporter implementations.
 *
 * <p>Initial Date: 06.01.2010 <br>
 *
 * @author Stefan
 */
public class LogLineConverter {

  /** the logging object used in this class * */
  private static final OLog log_ = Tracing.createLoggerFor(LogLineConverter.class);

  /**
   * spring property defining all properties - including the order in which they will be exported *
   */
  private List<String> orderedExportedProperties = new ArrayList<String>();

  /**
   * spring property defining all properties which should be anonymized - they must also be in
   * orderedExportedProperties *
   */
  private Set<String> anonymizedProperties = new HashSet<String>();

  /**
   * internal property which contains (javax.bean) PropertyDescriptors of each of the above property
   * - given the properties are available
   */
  private List<PropertyDescriptor> orderedExportedPropertyDescriptors =
      new ArrayList<PropertyDescriptor>();

  /**
   * spring property setter for orderedExportedProperties - which is the list of all properties to
   * be extracted from the LoggingObject and exported in the csv format
   *
   * @param orderedExportedProperties
   */
  public void setOrderedExportedProperties(List<String> orderedExportedProperties) {
    this.orderedExportedProperties = orderedExportedProperties;
    initPropertyDescriptor();
  }

  /**
   * spring property setter for anonymizedProperties - all of these must also be in the
   * orderedExportedProperties list
   */
  public void setAnonymizedProperties(Set<String> anonymizedProperties) {
    this.anonymizedProperties = anonymizedProperties;
  }

  /**
   * Returns a String which capitalizes the first letter of the string. (c) from
   * java.beans.NameGenerator (which unfortunatelly is not a public class)
   */
  public static String capitalize(String name) {
    if (name == null || name.length() == 0) {
      return name;
    }
    return name.substring(0, 1).toUpperCase(ENGLISH) + name.substring(1);
  }

  /** Initialize the (java.bean) PropertyDescriptors for the properties */
  private void initPropertyDescriptor() {
    for (Iterator<String> it = orderedExportedProperties.iterator(); it.hasNext(); ) {
      String propertyName = it.next();
      try {
        // we're using this specialized constructor since we want to allow properties that are
        // read-only (such as creationDate).
        // with the simpler constructor (String,Class) that would result in an Exception.
        // also note that we're using "is"+propertyName rather than get - that's the way the
        // PropertyDescriptor itself
        // does it in the constructor (String,Class) - resulting in a lookup of an is Method first
        // and then the get Method
        // this seems to be the correct standard here.
        PropertyDescriptor pd =
            new PropertyDescriptor(
                propertyName, LoggingObject.class, "is" + capitalize(propertyName), null);
        orderedExportedPropertyDescriptors.add(pd);
      } catch (IntrospectionException e) {
        log_.error(
            "initPropertyDescriptor: Could not retrieve property "
                + propertyName
                + " from LoggingObject, configuration error?",
            e);
      }
    }
  }

  /**
   * Returns the CSV Header line containing all property names in the exact same way as in the
   * config file - excluding those properties which could not be retrieved, i.e. for which no
   * PropertyDescriptor could be created
   *
   * @return the CSV Header line containing all property names in the exact same way as in the
   *     config file - excluding those properties which could not be retrieved, i.e. for which no
   *     PropertyDescriptor could be created
   */
  public String getCSVHeader() {
    List<String> propertyNames = new ArrayList<String>();
    for (Iterator<PropertyDescriptor> it = orderedExportedPropertyDescriptors.iterator();
        it.hasNext(); ) {
      PropertyDescriptor pd = it.next();
      propertyNames.add(pd.getName());
    }
    return StringHelper.formatAsCSVString(propertyNames);
  }

  /**
   * Returns a CSV line for the given LoggingObject - containing all properties in the exact same
   * way as in the config file - excluding those which could not be retrieved, i.e. for which no
   * PropertyDescriptor could be created
   *
   * @param loggingObject the LoggingObject for which a CSV line should be created
   * @return the CSV line representing the given LoggingObject
   */
  public String getCSVRow(LoggingObject loggingObject, boolean anonymize, Long resourceableId) {
    List<String> loggingObjectList = new ArrayList<String>();
    for (Iterator<PropertyDescriptor> it = orderedExportedPropertyDescriptors.iterator();
        it.hasNext(); ) {
      PropertyDescriptor pd = it.next();

      String strValue = "";
      try {
        Object value = pd.getReadMethod().invoke(loggingObject, (Object[]) null);
        if (value != null) {
          strValue = String.valueOf(value);
        }
        if (anonymize && anonymizedProperties.contains(pd.getName())) {
          // do anonymization
          strValue = makeAnonymous(String.valueOf(value), resourceableId);
        }
      } catch (IllegalArgumentException e) {
        // nothing to do
      } catch (IllegalAccessException e) {
        // nothing to do
      } catch (InvocationTargetException e) {
        // nothing to do
      }
      loggingObjectList.add(strValue);
    }

    return StringHelper.formatAsCSVString(loggingObjectList);
  }

  /**
   * encode a string and course resourcable id with MD5
   *
   * @param s
   * @param courseResId
   * @return
   */
  private String makeAnonymous(String s, Long courseResId) {
    String encodeValue = s + "-" + Long.toString(courseResId);
    // encode with MD5
    return Encoder.md5hash(encodeValue);
  }
}
Example #30
0
/**
 * Description:<br>
 * This implementation of the VersionsManager saved the revisions of a file in a file with the same
 * name as the original + ".xml". This xml file is saved in a parallel folder .version under the
 * root defined in FolderConfig. Every revision'file have a name made of a generated unique id + the
 * name of the original file.
 *
 * <p>Initial Date: 21 sept. 2009 <br>
 *
 * @author srosse
 */
public class VersionsFileManager extends VersionsManager implements Initializable {
  private static final OLog log = Tracing.createLoggerFor(VersionsFileManager.class);

  private static final Versions NOT_VERSIONED = new NotVersioned();
  private static final Pattern TAG_PATTERN = Pattern.compile("\\s*[<>]\\s*");
  private static XStream mystream;

  private File rootFolder;
  private File rootVersionFolder;
  private VFSContainer rootVersionsContainer;

  private FolderVersioningConfigurator versioningConfigurator;

  /** [spring] */
  private VersionsFileManager() {
    INSTANCE = this;
  }

  /**
   * [used by Spring]
   *
   * @param versioningConfigurator
   */
  public void setVersioningConfigurator(FolderVersioningConfigurator versioningConfigurator) {
    this.versioningConfigurator = versioningConfigurator;
  }

  @Override
  public Versions createVersionsFor(VFSLeaf leaf) {
    return createVersionsFor(leaf, false);
  }

  @Override
  public Versions createVersionsFor(VFSLeaf leaf, boolean force) {
    if (!(leaf instanceof Versionable)) {
      return NOT_VERSIONED;
    } else if (isVersionFile(leaf)) {
      return NOT_VERSIONED;
    }

    Versions versions = readVersions(leaf, false);
    return versions;
  }

  @Override
  public List<Versions> getDeletedFiles(VFSContainer container) {
    List<Versions> deletedRevisions = new ArrayList<Versions>();

    VFSContainer versionContainer = getCanonicalVersionFolder(container, false);
    if (versionContainer != null) {
      Set<String> currentNames = new HashSet<String>();
      for (VFSItem item : container.getItems(new VFSLeafFilter())) {
        currentNames.add(item.getName() + ".xml");
      }

      List<VFSItem> versionItems =
          versionContainer.getItems(new VFSItemSuffixFilter(new String[] {"xml"}));
      for (VFSItem versionItem : versionItems) {
        String name = versionItem.getName();
        if (versionItem instanceof VFSLeaf
            && !currentNames.contains(name)
            && isVersionsXmlFile((VFSLeaf) versionItem)) {
          Versions versions = readVersions(null, (VFSLeaf) versionItem);
          if (versions != null) {
            List<VFSRevision> revisions = versions.getRevisions();
            if (!revisions.isEmpty()) {
              deletedRevisions.add(versions);
            }
          }
        }
      }
    }
    return deletedRevisions;
  }

  private Versions readVersions(VFSLeaf leaf, boolean create) {
    VFSLeaf fVersions = getCanonicalVersionXmlFile(leaf, create);
    if (!create && fVersions == null) {
      VersionsFileImpl versions = new VersionsFileImpl();
      versions.setCurrentVersion((Versionable) leaf);
      versions.setVersioned(isVersioned(leaf));
      versions.setRevisionNr(getNextRevisionNr(versions));
      return versions;
    }
    return readVersions(leaf, fVersions);
  }

  private boolean isVersionsXmlFile(VFSLeaf fVersions) {
    if (fVersions == null || !fVersions.exists()) {
      return false;
    }
    InputStream in = fVersions.getInputStream();
    if (in == null) {
      return false;
    }

    Scanner scanner = new Scanner(in);
    scanner.useDelimiter(TAG_PATTERN);

    boolean foundVersionsTag = false;
    while (scanner.hasNext()) {
      String tag = scanner.next();
      if ("versions".equals(tag)) {
        foundVersionsTag = true;
        break;
      }
    }

    scanner.close();
    IOUtils.closeQuietly(in);
    return foundVersionsTag;
  }

  private Versions readVersions(VFSLeaf leaf, VFSLeaf fVersions) {
    if (fVersions == null) {
      return new NotVersioned();
    }

    try {
      VFSContainer fVersionContainer = fVersions.getParentContainer();
      VersionsFileImpl versions = (VersionsFileImpl) XStreamHelper.readObject(mystream, fVersions);
      versions.setVersionFile(fVersions);
      versions.setCurrentVersion((Versionable) leaf);
      if (versions.getRevisionNr() == null || versions.getRevisionNr().length() == 0) {
        versions.setRevisionNr(getNextRevisionNr(versions));
      }

      for (VFSRevision revision : versions.getRevisions()) {
        RevisionFileImpl revisionImpl = (RevisionFileImpl) revision;
        revisionImpl.setContainer(fVersionContainer);
      }
      return versions;
    } catch (Exception e) {
      log.warn("This file is not a versions XML file: " + fVersions, e);
      fVersions.delete();
      VersionsFileImpl versions = new VersionsFileImpl();
      versions.setCurrentVersion((Versionable) leaf);
      versions.setVersioned(isVersioned(leaf));
      versions.setRevisionNr(getNextRevisionNr(versions));
      log.warn("Deleted corrupt version XML file and created new version XML file: " + versions);
      // the old revisions can not be restored automatically. They are still on disk, you could
      // recover them
      // manually. This is not a perfect solution, but at least the user does not get an RS
      return versions;
    }
  }

  @Override
  public boolean addVersion(
      Versionable currentVersion, Identity identity, String comment, InputStream newFile) {
    VFSLeaf currentFile = (VFSLeaf) currentVersion;
    if (addToRevisions(currentVersion, identity, comment)) {
      // copy the content of the new file to the old
      boolean closeInputStream =
          !(newFile instanceof net.sf.jazzlib.ZipInputStream
              || newFile instanceof java.util.zip.ZipInputStream);
      if (VFSManager.copyContent(newFile, currentFile, closeInputStream)) {
        return true;
      }
    } else {
      log.error("Cannot create a version of this file: " + currentVersion);
    }
    return false;
  }

  @Override
  public boolean move(VFSLeaf currentFile, VFSLeaf targetFile, Identity author) {
    VFSLeaf fCurrentVersions = getCanonicalVersionXmlFile(currentFile, true);
    Versions currentVersions = readVersions(currentFile, fCurrentVersions);

    boolean brandNewVersionFile = false;
    VFSLeaf fTargetVersions = getCanonicalVersionXmlFile(targetFile, false);
    if (fTargetVersions == null) {
      brandNewVersionFile = true;
      fTargetVersions = getCanonicalVersionXmlFile(targetFile, true);
    }

    Versions targetVersions = readVersions(targetFile, fTargetVersions);
    if (!(currentVersions instanceof VersionsFileImpl)
        || !(targetVersions instanceof VersionsFileImpl)) {
      return false;
    }

    VersionsFileImpl targetVersionsImpl = (VersionsFileImpl) targetVersions;
    if (author != null) {
      targetVersionsImpl.setAuthor(author.getName());
    }
    if (brandNewVersionFile) {
      targetVersionsImpl.setCreator(currentVersions.getCreator());
      targetVersionsImpl.setComment(currentVersions.getComment());
    }

    boolean allOk = true;
    for (VFSRevision revision : currentVersions.getRevisions()) {
      allOk &= copyRevision(revision, fTargetVersions, targetVersionsImpl);
    }

    targetVersionsImpl.setRevisionNr(getNextRevisionNr(targetVersionsImpl));
    XStreamHelper.writeObject(mystream, fTargetVersions, targetVersionsImpl);

    return allOk;
  }

  private boolean copyRevision(
      VFSRevision revision, VFSLeaf fNewVersions, VersionsFileImpl targetVersions) {
    if (!(revision instanceof RevisionFileImpl)) {
      logWarn("Copy only copy persisted revisions", null);
    }

    RevisionFileImpl revisionImpl = (RevisionFileImpl) revision;
    String revUuid = revisionImpl.getUuid();
    for (VFSRevision rev : targetVersions.getRevisions()) {
      if (rev instanceof RevisionFileImpl) {
        RevisionFileImpl fRev = (RevisionFileImpl) rev;
        if (StringHelper.containsNonWhitespace(fRev.getUuid()) && fRev.getUuid().equals(revUuid)) {
          return true;
        }
      }
    }

    String uuid = UUID.randomUUID().toString().replace("-", "") + "_" + revision.getName();

    RevisionFileImpl newRevision = new RevisionFileImpl();
    newRevision.setName(revision.getName());
    newRevision.setFilename(uuid);
    newRevision.setRevisionNr(getNextRevisionNr(targetVersions));
    newRevision.setComment(revision.getComment());
    newRevision.setAuthor(revision.getAuthor());
    newRevision.setLastModified(revision.getLastModified());
    newRevision.setUuid(revUuid);

    // copy -> the files revision
    InputStream revisionIn = revision.getInputStream();

    VFSLeaf target = fNewVersions.getParentContainer().createChildLeaf(uuid);
    if (VFSManager.copyContent(revisionIn, target)) {
      targetVersions.setComment(revision.getComment());
      targetVersions.getRevisions().add(newRevision);
      targetVersions.setRevisionNr(getNextRevisionNr(targetVersions));
      targetVersions.setAuthor(revision.getAuthor());
      return true;
    }
    return false;
  }

  @Override
  public boolean move(Versionable currentVersion, VFSContainer container) {
    VFSLeaf currentFile = (VFSLeaf) currentVersion;
    VFSLeaf fVersions = getCanonicalVersionXmlFile(currentFile, true);
    Versions versions = readVersions(currentFile, fVersions);

    VFSContainer versionContainer = getCanonicalVersionFolder(container, true);

    boolean allOk = VFSConstants.YES.equals(versionContainer.copyFrom(fVersions));
    for (VFSRevision revision : versions.getRevisions()) {
      RevisionFileImpl revisionImpl = (RevisionFileImpl) revision;
      VFSLeaf revisionFile = revisionImpl.getFile();
      if (revisionFile != null) {
        allOk &= VFSConstants.YES.equals(versionContainer.copyFrom(revisionFile));
      }
    }

    allOk &= VFSConstants.YES.equals(fVersions.delete());
    for (VFSRevision revision : versions.getRevisions()) {
      VFSLeaf revisionFile = ((RevisionFileImpl) revision).getFile();
      if (revisionFile != null) {
        allOk &= VFSConstants.YES.equals(revisionFile.delete());
      }
    }
    return allOk;
  }

  @Override
  public boolean restore(Versionable currentVersion, VFSRevision version, String comment) {
    VFSLeaf currentFile = (VFSLeaf) currentVersion;
    if (!VFSManager.exists(currentFile)) {
      return false;
    }

    // add current version to versions file
    if (addToRevisions(currentVersion, null, comment)) {
      // copy the content of the new file to the old
      if (VFSManager.copyContent(version.getInputStream(), currentFile)) {
        return true;
      }
    } else {
      log.error("Cannot create a version of this file: " + currentVersion);
    }

    return false;
  }

  @Override
  public boolean restore(VFSContainer container, VFSRevision revision) {
    String filename = revision.getName();
    VFSItem restoredItem = container.resolve(filename);
    if (restoredItem == null) {
      restoredItem = container.createChildLeaf(filename);
    }
    if (restoredItem instanceof VFSLeaf) {
      VFSLeaf restoredLeaf = (VFSLeaf) restoredItem;
      InputStream inStream = revision.getInputStream();
      if (VFSManager.copyContent(inStream, restoredLeaf)) {
        VFSLeaf versionFile = getCanonicalVersionXmlFile(restoredLeaf, true);
        Versions versions = readVersions(restoredLeaf, versionFile);
        if (versions instanceof VersionsFileImpl) {
          versions.getRevisions().remove(revision);
          ((VersionsFileImpl) versions).setRevisionNr(getNextRevisionNr(versions));
        }
        XStreamHelper.writeObject(mystream, versionFile, versions);
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean deleteRevisions(Versionable currentVersion, List<VFSRevision> versionsToDelete) {
    VFSLeaf currentFile = (VFSLeaf) currentVersion;
    Versions versions = readVersions(currentFile, true);
    List<VFSRevision> allVersions = versions.getRevisions();

    Map<String, VFSLeaf> filenamesToDelete = new HashMap<String, VFSLeaf>(allVersions.size());
    for (VFSRevision versionToDelete : versionsToDelete) {
      RevisionFileImpl versionImpl = (RevisionFileImpl) versionToDelete;
      for (Iterator<VFSRevision> allVersionIt = allVersions.iterator(); allVersionIt.hasNext(); ) {
        RevisionFileImpl allVersionImpl = (RevisionFileImpl) allVersionIt.next();
        if (allVersionImpl.getFilename() != null
            && allVersionImpl.getFilename().equals(versionImpl.getFilename())) {
          allVersionIt.remove();
          break;
        }
      }

      VFSLeaf fileToDelete = versionImpl.getFile();
      if (fileToDelete != null) {
        filenamesToDelete.put(fileToDelete.getName(), fileToDelete);
      }
    }

    List<RevisionFileImpl> missingFiles = new ArrayList<>();
    for (VFSRevision survivingVersion : allVersions) {
      RevisionFileImpl survivingVersionImpl = (RevisionFileImpl) survivingVersion;
      VFSLeaf revFile = survivingVersionImpl.getFile();
      if (revFile == null) {
        missingFiles.add(survivingVersionImpl); // file is missing
      } else if (filenamesToDelete.containsKey(revFile.getName())) {
        filenamesToDelete.remove(revFile.getName());
      }
    }
    if (missingFiles.size() > 0) {
      allVersions.removeAll(missingFiles);
    }

    for (VFSLeaf fileToDelete : filenamesToDelete.values()) {
      fileToDelete.deleteSilently();
    }

    VFSLeaf versionFile = getCanonicalVersionXmlFile(currentFile, true);
    XStreamHelper.writeObject(mystream, versionFile, versions);
    if (currentVersion.getVersions() instanceof VersionsFileImpl) {
      ((VersionsFileImpl) currentVersion.getVersions()).update(versions);
    }
    return true;
  }

  @Override
  public boolean deleteVersions(List<Versions> versions) {
    for (Versions versionToDelete : versions) {
      if (versionToDelete instanceof VersionsFileImpl) {
        VersionsFileImpl versionsImpl = (VersionsFileImpl) versionToDelete;
        VFSLeaf versionFile = versionsImpl.getVersionFile();
        if (versionFile != null) {
          // robust against manual file system manipulation
          versionFile.deleteSilently();
        }
        for (VFSRevision revisionToDelete : versionsImpl.getRevisions()) {
          RevisionFileImpl versionImpl = (RevisionFileImpl) revisionToDelete;
          VFSLeaf fileToDelete = versionImpl.getFile();
          if (fileToDelete != null) {
            fileToDelete.deleteSilently();
          }
        }
      }
    }
    return true;
  }

  @Override
  public boolean delete(VFSItem item, boolean force) {
    if (item instanceof VFSContainer) {
      if (force) {
        VFSContainer container = (VFSContainer) item;
        VFSContainer versionContainer = getCanonicalVersionFolder(container, false);
        if (versionContainer == null) {
          return true;
        }
        return VFSConstants.YES.equals(versionContainer.delete());
      }
      return true;
    } else if (item instanceof VFSLeaf && item instanceof Versionable) {
      VFSLeaf leaf = (VFSLeaf) item;
      if (force || isTemporaryFile(leaf)) {
        cleanUp(leaf);
      } else {
        Identity identity = ThreadLocalUserActivityLogger.getLoggedIdentity();
        addToRevisions((Versionable) leaf, identity, null);
      }
    }
    return false;
  }

  /**
   * Some temporary/lock files of specific editors need to be force deleted with all versions. Word
   * can reuse older names.
   *
   * @param leaf
   * @return
   */
  private boolean isTemporaryFile(VFSLeaf leaf) {
    String name = leaf.getName();
    // temporary files
    if (name.endsWith(".tmp")) {
      // Word 2010: ~WRD0002.tmp
      if (name.startsWith("~WRD") || name.startsWith("~WRL")) {
        return true;
      }
      // PowerPoint 2010: ppt5101.tmp
      if (name.startsWith("ppt")) {
        return true;
      }
    }
    // lock files of Word 2010, Excel 2010, PowerPoint 2010:
    if (name.startsWith("~$")
        && (name.endsWith(".docx") || name.endsWith(".xlsx") || name.endsWith(".pptx"))) {
      return true;
    }

    // OpenOffice locks: .~lock.Versions_21.odt#
    if (name.startsWith(".~lock.")
        && (name.endsWith(".odt#") /* Writer */
            || name.endsWith(".ods#") /* Calc */
            || name.endsWith(".odp#") /* Impress */
            || name.endsWith("odf#") /* Math */
            || name.endsWith(".odg#") /* Draw */)) {
      return true;
    }
    // OpenOffice database lock
    if (name.endsWith(".odb.lck")) {
      return true;
    }

    return false;
  }

  /**
   * Clean up all revisions files, xml file
   *
   * @param leaf
   */
  private void cleanUp(VFSLeaf leaf) {
    String relPath = getRelPath(leaf);
    if (relPath == null) return; // cannot handle

    File fVersion = new File(getRootVersionsFile(), relPath + ".xml");
    File fParentVersion = fVersion.getParentFile();
    if (!fParentVersion.exists()) return; // already deleted

    VFSLeaf versionLeaf = null;
    if (fVersion.exists()) {
      LocalFolderImpl localVersionContainer = new LocalFolderImpl(fParentVersion);
      versionLeaf = (VFSLeaf) localVersionContainer.resolve(fVersion.getName());
    }

    if (versionLeaf == null) return; // already deleted
    Versions versions = readVersions(leaf, versionLeaf);
    for (VFSRevision versionToDelete : versions.getRevisions()) {
      RevisionFileImpl versionImpl = (RevisionFileImpl) versionToDelete;
      VFSLeaf fileToDelete = versionImpl.getFile();
      if (fileToDelete != null) {
        fileToDelete.delete();
      }
    }
    versionLeaf.delete();
  }

  @Override
  public boolean rename(VFSItem item, String newname) {
    if (item instanceof VFSLeaf) {
      VFSLeaf currentFile = (VFSLeaf) item;
      VFSLeaf versionFile = getCanonicalVersionXmlFile(currentFile, true);
      // infinite loop if rename is own versions file
      return VFSConstants.YES.equals(versionFile.rename(newname + ".xml"));
    } else if (item instanceof VFSContainer) {
      VFSContainer container = (VFSContainer) item;
      VFSContainer versionContainer = getCanonicalVersionFolder(container, false);
      if (versionContainer == null) {
        return true;
      }
      return VFSConstants.YES.equals(versionContainer.rename(newname));
    }
    return false;
  }

  /**
   * @see
   *     org.olat.core.util.vfs.version.VersionsManager#addToRevisions(org.olat.core.util.vfs.version.Versionable,
   *     org.olat.core.id.Identity, java.lang.String)
   */
  @Override
  public boolean addToRevisions(Versionable currentVersion, Identity identity, String comment) {
    int maxNumOfVersions = versioningConfigurator.getMaxNumOfVersionsAllowed();
    if (maxNumOfVersions == 0) {
      return true; // deactivated, return all ok
    }

    VFSLeaf currentFile = (VFSLeaf) currentVersion;

    VFSLeaf versionFile = getCanonicalVersionXmlFile(currentFile, true);
    if (versionFile == null) {
      return false; // cannot do something with the current file
    }

    VFSContainer versionContainer = versionFile.getParentContainer();

    String name = currentFile.getName();

    // read from the
    Versions v = readVersions(currentFile, versionFile);
    if (!(v instanceof VersionsFileImpl)) {
      log.error("Wrong implementation of Versions: " + v);
      return false;
    }
    VersionsFileImpl versions = (VersionsFileImpl) v;
    boolean sameFile = isSameFile(currentFile, versions);
    String uuid =
        sameFile ? getLastRevisionFilename(versions) : UUID.randomUUID().toString() + "_" + name;

    String versionNr = getNextRevisionNr(versions);
    String currentAuthor = versions.getAuthor();
    long lastModifiedDate = 0;
    if (currentFile instanceof MetaTagged) {
      MetaInfo metaInfo = ((MetaTagged) currentFile).getMetaInfo();
      if (metaInfo != null) {
        metaInfo.clearThumbnails();
        if (currentAuthor == null) {
          currentAuthor = metaInfo.getAuthor();
        }
        lastModifiedDate = metaInfo.getLastModified();
      }
    }

    if (lastModifiedDate <= 0) {
      Calendar cal = Calendar.getInstance();
      cal.setTime(new Date());
      lastModifiedDate = cal.getTimeInMillis();
    }

    RevisionFileImpl newRevision = new RevisionFileImpl();
    newRevision.setUuid(UUID.randomUUID().toString());
    newRevision.setName(name);
    newRevision.setFilename(uuid);
    newRevision.setRevisionNr(versionNr);
    newRevision.setComment(versions.getComment());
    newRevision.setAuthor(currentAuthor);
    newRevision.setLastModified(lastModifiedDate);

    if (versions.getRevisions().isEmpty() && currentVersion instanceof MetaTagged) {
      MetaTagged metaTagged = (MetaTagged) currentVersion;
      versions.setCreator(metaTagged.getMetaInfo().getAuthor());
    }

    if (sameFile || VFSManager.copyContent(currentFile, versionContainer.createChildLeaf(uuid))) {
      if (identity != null) {
        versions.setAuthor(identity.getName());
      }

      if (maxNumOfVersions >= 0 && versions.getRevisions().size() >= maxNumOfVersions) {
        List<VFSRevision> revisions = versions.getRevisions();
        int numOfVersionsToDelete =
            Math.min(revisions.size(), (revisions.size() - maxNumOfVersions) + 1);
        if (numOfVersionsToDelete > 0) {
          List<VFSRevision> versionsToDelete = revisions.subList(0, numOfVersionsToDelete);
          deleteRevisions(currentVersion, versionsToDelete);
          versions = (VersionsFileImpl) currentVersion.getVersions();
        }
      }
      versions.setComment(comment);
      versions.getRevisions().add(newRevision);
      versions.setRevisionNr(getNextRevisionNr(versions));
      XStreamHelper.writeObject(mystream, versionFile, versions);
      if (currentVersion.getVersions() instanceof VersionsFileImpl) {
        ((VersionsFileImpl) currentVersion.getVersions()).update(versions);
      }
      return true;
    } else {
      log.error("Cannot create a version of this file: " + currentVersion);
    }
    return false;
  }

  private boolean isSameFile(VFSLeaf currentFile, VersionsFileImpl versions) {
    boolean same = false;
    if (versions.getRevisions() != null && !versions.getRevisions().isEmpty()) {
      VFSRevision lastRevision = versions.getRevisions().get(versions.getRevisions().size() - 1);

      long lastSize = lastRevision.getSize();
      long currentSize = currentFile.getSize();
      if (currentSize == lastSize
          && currentSize > 0
          && lastRevision instanceof RevisionFileImpl
          && currentFile instanceof LocalFileImpl) {
        RevisionFileImpl lastRev = ((RevisionFileImpl) lastRevision);
        LocalFileImpl current = (LocalFileImpl) currentFile;
        // can be the same file
        try {
          Checksum cm1 =
              FileUtils.checksum(((LocalFileImpl) lastRev.getFile()).getBasefile(), new Adler32());
          Checksum cm2 = FileUtils.checksum(current.getBasefile(), new Adler32());
          same = cm1.getValue() == cm2.getValue();
        } catch (IOException e) {
          log.debug("Error calculating the checksum of files");
        }
      }
    }
    return same;
  }

  public String getNextRevisionNr(Versions versions) {
    int maxNumber = 0;
    for (VFSRevision version : versions.getRevisions()) {
      String versionNr = version.getRevisionNr();
      if (versionNr != null && versionNr.length() > 0) {
        try {
          int number = Integer.parseInt(versionNr);
          maxNumber = Math.max(maxNumber, number);
        } catch (Exception ex) {
          // if not a number, don't interest us
        }
      }
    }
    return Integer.toString(maxNumber + 1);
  }

  private String getLastRevisionFilename(Versions versions) {
    if (versions.getRevisions() == null || versions.getRevisions().isEmpty()) {
      return null;
    }

    VFSRevision revision = versions.getRevisions().get(versions.getRevisions().size() - 1);
    if (revision instanceof RevisionFileImpl) {
      return ((RevisionFileImpl) revision).getFilename();
    }
    return null;
  }

  /**
   * Get the canonical path to the file's meta file.
   *
   * @param bcPath
   * @return String
   */
  private VFSLeaf getCanonicalVersionXmlFile(VFSItem item, boolean create) {
    File f = getOriginFile(item);
    if (!f.exists()) {
      return null;
    }

    String relPath = getRelPath(item);
    if (relPath == null) {
      // cannot handle
      return null;
    }

    File fVersion = new File(getRootVersionsFile(), relPath + ".xml");
    File fParentVersion = fVersion.getParentFile();
    if (!fParentVersion.exists() && create) {
      fParentVersion.mkdirs();
    }

    if (fVersion.exists()) {
      LocalFolderImpl localVersionContainer = new LocalFolderImpl(fParentVersion);
      return (VFSLeaf) localVersionContainer.resolve(fVersion.getName());
    } else if (create) {
      LocalFolderImpl localVersionContainer = new LocalFolderImpl(fParentVersion);
      VersionsFileImpl versions = new VersionsFileImpl();
      versions.setVersioned(isVersioned(item));
      versions.setRevisionNr(getNextRevisionNr(versions));
      VFSLeaf fVersions = localVersionContainer.createChildLeaf(fVersion.getName());
      XStreamHelper.writeObject(mystream, fVersions, versions);
      return fVersions;
    }
    return null;
  }

  protected VFSContainer getCanonicalVersionFolder(VFSContainer container, boolean create) {
    String relPath = getRelPath(container);
    File fVersion = new File(getRootVersionsFile(), relPath);
    if (fVersion.exists()) {
      return new LocalFolderImpl(fVersion);
    }
    if (create) {
      fVersion.mkdirs();
      return new LocalFolderImpl(fVersion);
    }
    return null;
  }

  private String getRelPath(VFSItem item) {
    String relPath = null;
    if (item instanceof NamedContainerImpl) {
      item = ((NamedContainerImpl) item).getDelegate();
    }
    if (item instanceof MergeSource) {
      item = ((MergeSource) item).getRootWriteContainer();
    }
    if (item instanceof OlatRelPathImpl) {
      relPath = ((OlatRelPathImpl) item).getRelPath();
    } else if (item instanceof LocalImpl) {
      LocalImpl impl = (LocalImpl) item;
      String absolutPath = impl.getBasefile().getAbsolutePath();
      if (absolutPath.startsWith(getCanonicalRoot())) {
        relPath = absolutPath.substring(getCanonicalRoot().length());
      }

      Path path = impl.getBasefile().toPath();
      Path relativePath = getCanonicalRootFile().toPath().relativize(path);
      String relPath2 = "/" + relativePath.toString();
      log.debug(relPath + " :: " + relPath2);
    }
    return relPath;
  }

  private boolean isVersionFile(VFSItem item) {
    File f = getOriginFile(item);
    if (f == null) return false;

    try {
      String path = f.getCanonicalPath();
      String vPath = getRootVersionsFile().getCanonicalPath();
      return path.startsWith(vPath);
    } catch (IOException e) {
      log.error("Cannot check if this file is a version file: " + item, e);
      return false;
    }
  }

  private boolean isVersioned(VFSItem item) {
    if (item == null) return false;
    VFSContainer parent = item.getParentContainer();
    return FolderConfig.versionsEnabled(parent);
  }

  private File getOriginFile(VFSItem item) {
    if (item instanceof LocalImpl) {
      LocalImpl localImpl = (LocalImpl) item;
      return localImpl.getBasefile();
    }
    if (item instanceof OlatRelPathImpl) {
      OlatRelPathImpl relPath = (OlatRelPathImpl) item;
      return new File(getCanonicalRoot(), relPath.getRelPath());
    }
    return null;
  }

  public File getCanonicalRootFile() {
    if (rootFolder == null) {
      rootFolder = new File(FolderConfig.getCanonicalRoot());
    }
    return rootFolder;
  }

  public String getCanonicalRoot() {
    return getCanonicalRootFile().getAbsolutePath();
  }

  public File getRootVersionsFile() {
    if (rootVersionsContainer == null) {
      rootVersionFolder = new File(FolderConfig.getCanonicalVersionRoot());
      if (!rootVersionFolder.exists()) {
        rootVersionFolder.mkdirs();
      }
      rootVersionsContainer = new LocalFolderImpl(rootVersionFolder);
    }
    return rootVersionFolder;
  }

  public VFSContainer getRootVersionsContainer() {
    if (rootVersionsContainer == null) {
      rootVersionFolder = new File(FolderConfig.getCanonicalVersionRoot());
      if (!rootVersionFolder.exists()) {
        rootVersionFolder.mkdirs();
      }
      rootVersionsContainer = new LocalFolderImpl(rootVersionFolder);
    }
    return rootVersionsContainer;
  }

  @Override
  public int countDirectories() {
    VFSContainer versionsContainer = getRootVersionsContainer();
    if (versionsContainer.exists()) {
      return countDirectories(versionsContainer);
    }
    return 0;
  }

  private int countDirectories(VFSContainer container) {
    int count = 1; // itself
    List<VFSItem> children = container.getItems(new SystemItemFilter());
    for (VFSItem child : children) {
      if (child instanceof VFSContainer) {
        count += countDirectories((VFSContainer) child);
      }
    }
    return count;
  }

  @Override
  public void pruneHistory(long maxHistoryLength, ProgressDelegate progress) {
    VFSContainer versionsContainer = getRootVersionsContainer();
    if (!versionsContainer.exists()) {
      return;
    }
    // delete folder without versioning first

    int count = 0;
    String[] excludedRootFolders = new String[] {"tmp", "scorm", "forum", "portfolio"};
    for (String excludedRootFolder : excludedRootFolders) {
      VFSItem excludedContainer = versionsContainer.resolve(excludedRootFolder);
      if (excludedContainer instanceof LocalFolderImpl) {
        File excludedFile = ((LocalFolderImpl) excludedContainer).getBasefile();
        FileUtils.deleteQuietly(excludedFile);
        if (progress != null) progress.setInfo(excludedContainer.getName());
      }
      if (progress != null) progress.setActual(++count);
    }

    if (maxHistoryLength < 0) {
      // nothing to do
    } else if (maxHistoryLength == 0 && versionsContainer instanceof LocalFolderImpl) {
      // delete all the stuff
      FileUtils.deleteQuietly(((LocalFolderImpl) versionsContainer).getBasefile());
    } else {
      pruneVersionHistory(versionsContainer, maxHistoryLength, progress, count);
    }

    if (progress != null) progress.finished();
  }

  private void pruneVersionHistory(
      VFSContainer container, long maxHistoryLength, ProgressDelegate progress, int count) {
    List<VFSItem> children = container.getItems(new SystemItemFilter());
    for (VFSItem child : children) {
      if (child instanceof VFSContainer) {
        if (progress != null) progress.setActual(++count);
        pruneVersionHistory((VFSContainer) child, maxHistoryLength, progress, count);
      }
      if (child instanceof VFSLeaf) {
        VFSLeaf versionsLeaf = (VFSLeaf) child;
        pruneVersionHistory(versionsLeaf, maxHistoryLength, progress);
      }
    }
  }

  private void pruneVersionHistory(
      VFSLeaf versionsLeaf, long maxHistoryLength, ProgressDelegate progress) {
    if (versionsLeaf.getName().endsWith(".xml") && isVersionsXmlFile(versionsLeaf)) {
      File originalFile = reversedOriginFile(versionsLeaf);
      if (originalFile.exists()) {
        VFSLeaf original = new LocalFileImpl(originalFile);
        if (progress != null) progress.setInfo(original.getName());
        Versions versions = readVersions(original, versionsLeaf);
        List<VFSRevision> revisions = versions.getRevisions();
        if (revisions.size() > maxHistoryLength) {
          List<VFSRevision> revisionsToDelete =
              revisions.subList(0, revisions.size() - (int) maxHistoryLength);
          deleteRevisions((Versionable) original, revisionsToDelete);
        }
      }
    }
  }

  @Override
  public boolean deleteOrphans(ProgressDelegate progress) {
    List<OrphanVersion> orphans = orphans();
    if (progress != null) progress.setMax(orphans.size());
    int count = 0;
    for (OrphanVersion orphan : orphans) {
      delete(orphan);
      if (progress != null) {
        progress.setActual(++count);
        progress.setInfo(orphan.getOriginalFilePath());
      }
    }
    if (progress != null) progress.finished();
    return true;
  }

  @Override
  public boolean delete(OrphanVersion orphan) {
    VFSLeaf versionLeaf = orphan.getVersionsLeaf();

    if (versionLeaf == null) return true; // already deleted
    Versions versions = orphan.getVersions();
    for (VFSRevision versionToDelete : versions.getRevisions()) {
      RevisionFileImpl versionImpl = (RevisionFileImpl) versionToDelete;
      versionImpl.setContainer(orphan.getVersionsLeaf().getParentContainer());
      VFSLeaf fileToDelete = versionImpl.getFile();
      if (fileToDelete != null) {
        fileToDelete.delete();
      }
    }
    versionLeaf.delete();
    return true;
  }

  @Override
  public List<OrphanVersion> orphans() {
    List<OrphanVersion> orphans = new ArrayList<OrphanVersion>();
    VFSContainer versionsContainer = getRootVersionsContainer();
    crawlForOrphans(versionsContainer, orphans);
    return orphans;
  }

  private void crawlForOrphans(VFSContainer container, List<OrphanVersion> orphans) {
    if (!container.exists()) {
      return;
    }

    List<VFSItem> children = container.getItems();
    for (VFSItem child : children) {
      if (child instanceof VFSContainer) {
        crawlForOrphans((VFSContainer) child, orphans);
      }
      if (child instanceof VFSLeaf) {
        VFSLeaf versionsLeaf = (VFSLeaf) child;
        if (child.getName().endsWith(".xml")) {
          Versions versions = isOrphan(versionsLeaf);
          if (versions == null) {
            continue;
          } else {
            List<VFSRevision> revisions = versions.getRevisions();
            if (revisions != null) {
              for (VFSRevision revision : revisions) {
                if (revision instanceof RevisionFileImpl) {
                  ((RevisionFileImpl) revision).setContainer(container);
                }
              }
            }
          }
          File originalFile = reversedOriginFile(child);
          if (!originalFile.exists()) {
            VFSLeaf orphan = new LocalFileImpl(originalFile);
            orphans.add(new OrphanVersion(orphan, versionsLeaf, versions));
          }
        }
      }
    }
  }

  private Versions isOrphan(VFSLeaf potentialOrphan) {
    try {
      if (potentialOrphan.exists()) {
        VersionsFileImpl versions =
            (VersionsFileImpl) XStreamHelper.readObject(mystream, potentialOrphan);
        return versions;
      }
      return null;
    } catch (Exception e) {
      return null;
    }
  }

  private File reversedOriginFile(VFSItem versionXml) {
    String path =
        File.separatorChar + versionXml.getName().substring(0, versionXml.getName().length() - 4);
    for (VFSContainer parent = versionXml.getParentContainer();
        parent != null && !parent.isSame(getRootVersionsContainer());
        parent = parent.getParentContainer()) {
      path = File.separatorChar + parent.getName() + path;
    }

    return new File(getCanonicalRoot(), path);
  }

  /** @see org.olat.core.configuration.Initializable#init() */
  public void init() {
    mystream = XStreamHelper.createXStreamInstance();
    mystream.alias("versions", VersionsFileImpl.class);
    mystream.alias("revision", RevisionFileImpl.class);
    mystream.omitField(VersionsFileImpl.class, "currentVersion");
    mystream.omitField(VersionsFileImpl.class, "versionFile");
    mystream.omitField(RevisionFileImpl.class, "current");
    mystream.omitField(RevisionFileImpl.class, "container");
    mystream.omitField(RevisionFileImpl.class, "file");
  }
}