All Downloads are FREE. Search and download functionalities are using the official Maven repository.

laika.io.config.IncludeHandler.scala Maven / Gradle / Ivy

/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package laika.io.config

import java.io.File
import java.net.URL
import cats.effect.{Async, Sync}
import cats.implicits._
import laika.config.Config.IncludeMap
import laika.config.{ConfigParser, ConfigResourceError}
import laika.io.runtime.Batch
import laika.parse.hocon.{IncludeAny, IncludeClassPath, IncludeFile, IncludeResource, IncludeUrl, ValidStringValue}

/** Internal utility that provides configuration files requested by include statements in other
  * configuration instances.
  * 
  * @author Jens Halm
  */
object IncludeHandler {
  
  case class RequestedInclude(resource: IncludeResource, parent: Option[IncludeResource])
  case class LoadedInclude(requestedResource: IncludeResource, resolvedResource: IncludeResource, result: Either[ConfigResourceError, String])

  /** Loads the requested resources and maps them to the request instance for later lookup.
    * 
    * If a resource is not present (e.g. file does not exist in the file system or HTTP call
    * produced a 404) then the requested resource will not be present as a key in the result map.
    * 
    * If a resource is present, but fails to load or parse correctly, the error will
    * be mapped to the requested resource as a `Left`. Successfully loaded and parsed
    * resources appear in the result map as a `Right`.
    */
  def load[F[_]: Async : Batch] (includes: Seq[RequestedInclude]): F[IncludeMap] = 
    
    if (includes.isEmpty) Sync[F].pure(Map.empty) else {
      
      def prepareFile(include: IncludeFile, requested: IncludeResource, parent: Option[IncludeResource]): F[(IncludeResource, IncludeResource)] = Sync[F].pure {
        if (new File(include.resourceId.value).isAbsolute) (include, requested)
        else parent.flatMap {
          case IncludeFile(id, _) => Option(new File(id.value).getParentFile)
          case _ => None
        } match {
          case Some(parentFile) => (IncludeFile(ValidStringValue(new File(parentFile, include.resourceId.value).getPath), include.isRequired), requested)
          case None => (include, requested)
        }
      }
      
      def prepareClasspath(include: IncludeClassPath, requested: IncludeResource, parent: Option[IncludeResource]): F[(IncludeResource, IncludeResource)] = Sync[F].pure {
        if (include.resourceId.value.startsWith("/")) (include.copy(resourceId = ValidStringValue(include.resourceId.value.drop(1))), include)
        else parent match {
          case Some(p: IncludeClassPath) if p.resourceId.value.contains("/") => 
            val parentPath = p.resourceId.value.substring(0, p.resourceId.value.lastIndexOf("/"))
            val childPath = s"$parentPath/${include.resourceId.value}"
            (IncludeClassPath(ValidStringValue(childPath), include.isRequired), requested)
          case _ => (include, requested)
        }
      }
      
      def prepareUrl(include: IncludeUrl, requested: IncludeResource, parent: Option[IncludeResource]): F[(IncludeResource, IncludeResource)] = Sync[F].delay {
        parent match {
          case Some(p: IncludeUrl) => 
            val parentUrl = new URL(p.resourceId.value)
            val childUrl = new URL(parentUrl, include.resourceId.value)
            (IncludeUrl(ValidStringValue(childUrl.toString), include.isRequired), requested)
          case _ => (include, requested)
        }
      }

      def prepareAny(include: IncludeAny, parent: Option[IncludeResource]): F[(IncludeResource, IncludeResource)] = 
        Sync[F].delay(new URL(include.resourceId.value))
          .flatMap(_ => prepareUrl(IncludeUrl(include.resourceId, include.isRequired), include, parent))
          .handleErrorWith { _ =>
            parent match {
              case Some(_: IncludeClassPath) => prepareClasspath(IncludeClassPath(include.resourceId, include.isRequired), include, parent)
              case Some(_: IncludeUrl)       => prepareUrl(IncludeUrl(include.resourceId, include.isRequired), include, parent)
              case _                         => prepareFile(IncludeFile(include.resourceId, include.isRequired), include, parent)
            }
          }
      
      val preparedIncludes = includes.map {
        case RequestedInclude(i: IncludeFile, parent)      => prepareFile(i, i, parent)
        case RequestedInclude(i: IncludeClassPath, parent) => prepareClasspath(i, i, parent)
        case RequestedInclude(i: IncludeUrl, parent)       => prepareUrl(i, i, parent)
        case RequestedInclude(i: IncludeAny, parent)       => prepareAny(i, parent)
      }.toVector.sequence
    
      def result(requestedResource: IncludeResource, resolvedResource: IncludeResource, result: F[Option[Either[ConfigResourceError, String]]]): F[Option[LoadedInclude]] =
        result.map(_.map(res => LoadedInclude(requestedResource, resolvedResource, res)))
      
      preparedIncludes.flatMap { includes =>
        Batch[F].execute(
          includes.map {
            case (i@IncludeFile(resourceId, _), orig)      => result(orig, i, ResourceLoader.loadFile(resourceId.value))
            case (i@IncludeClassPath(resourceId, _), orig) => result(orig, i, ResourceLoader.loadClasspathResource(resourceId.value))
            case (i@IncludeUrl(resourceId, _), orig)       => Sync[F].delay(new URL(resourceId.value))
                                                              .flatMap(url => result(orig, i, ResourceLoader.loadUrl(url)))
            case _                                       => Sync[F].pure(Option.empty[LoadedInclude])
          }
        )
      }.flatMap { loadedResources =>
        val configParsers = loadedResources.unite.map(loaded => (loaded.requestedResource, loaded.resolvedResource, loaded.result.map(ConfigParser.parse)))
        val includeMap = configParsers.map {
          case (requested, _, result) => 
            (requested, result.flatMap(_.unresolved))
        }.toMap
        val recursiveIncludes = configParsers.flatMap {
          case (_, resolved, Right(parser)) => parser.includes.filterNot(includeMap.contains).map(RequestedInclude(_, Some(resolved)))
          case _ => Nil
        }
        load(recursiveIncludes).map(_ ++ includeMap)
      }
      
    }
  
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy