com.nawforce.common.documents.DocumentIndex.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pkgforce_2.12 Show documentation
Show all versions of pkgforce_2.12 Show documentation
Salesforce Metadata Management Utility Library
The newest version!
/*
[The "BSD licence"]
Copyright (c) 2019 Kevin Jones
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.nawforce.common.documents
import com.nawforce.common.api.{Name, LoggerOps, TypeName}
import com.nawforce.common.diagnostics.{ERROR_CATEGORY, Issue, IssueLogger}
import com.nawforce.common.names.TypeNames
import com.nawforce.common.path.PathLike
import scala.collection.mutable
/** Index of an ordered set of Paths with duplicate detection.
*
* The duplicate detection is based on the relevant MetadataDocumentType(s) being able to generate an accurate TypeName
* for the metadata. Where multiple metadata items may contribute to a type, e.g. labels, make sure that
* duplicatesAllowed is set which will bypass the duplicate detection. Duplicates are reported as errors and then
* ignored.
*
* During an upsert/deletion of new types the index will also need to be updated so that it maintains an accurate
* view of the metadata files being used.
*/
class DocumentIndex(namespace: Option[Name], paths: Seq[PathLike], logger: IssueLogger, forceIgnore: Option[ForceIgnore] = None) {
/** All documents partitioned by declared extension */
private val documents = new mutable.HashMap[Name, mutable.HashMap[TypeName, List[MetadataDocument]]]()
/** The typeNames that may be exclusively generated by the documents, for duplicate detection */
private val typeNames = new mutable.HashSet[TypeName]()
index()
val size: Int = documents.values.map(_.size).sum
/** Get index'd metadata by declared extension */
def getByExtension(ext: Name): Set[MetadataDocument] = {
if (!documents.contains(ext)) return Set.empty
documents(ext).values.flatten.toSet
}
/** Get index'd metadata by declared extension */
def getByExtensionIterable(ext: Name): Iterable[MetadataDocument] = {
if (!documents.contains(ext)) return Set.empty
documents(ext).values.flatten
}
/* Find a class or trigger by its typename */
def getByType(typeName: TypeName): Option[MetadataDocument] = {
documents.get(MetadataDocument.clsExt).flatMap(_.get(typeName)).orElse(
documents.get(MetadataDocument.triggerExt).flatMap(_.get(typeName))
).map(_.head)
}
/** Upsert a metadata document with duplicate detection */
def upsert(metadata: MetadataDocument): Boolean = {
// Duplicates always good
if (metadata.duplicatesAllowed) {
addDocument(metadata)
return true
}
// Label replacement OK
val typeName = metadata.typeName(namespace)
if (typeName == TypeNames.Label)
return true
// New is OK
if (!typeNames.contains(typeName)) {
addDocument(metadata)
return true
}
// Existing with same path OK, but beware some files may have been deleted without notification
val knownDocs = documents.get(metadata.extension)
.flatMap(_.get(typeName)).getOrElse(Nil)
val docs = knownDocs.filter(_.path.exists)
if (docs.size != knownDocs.size)
documents(metadata.extension).put(typeName, docs)
if (docs.isEmpty || docs.contains(metadata)) {
return true
}
docs.foreach(doc => {
logger.log(Issue(ERROR_CATEGORY, LineLocationImpl(doc.path.toString, 0),
s"Duplicate type '$typeName' found in '${metadata.path}', ignoring this file"))
})
false
}
/** Remove a metadata document from the index */
def remove(metadataDocumentType: MetadataDocument): Unit = {
documents.get(metadataDocumentType.extension).foreach(docs => {
val typeName = metadataDocumentType.typeName(namespace)
if (!metadataDocumentType.duplicatesAllowed) {
docs.remove(typeName)
typeNames.remove(typeName)
} else {
val filtered = docs.getOrElse(typeName, Nil).filterNot(_.path == metadataDocumentType.path)
docs.put(typeName, filtered)
typeNames.remove(typeName)
}
})
}
private def index(): Unit = {
paths.reverse.filter(_.isDirectory).foreach(p => indexPath(p, forceIgnore))
createGhostSObjectFiles(Name("field"), forceIgnore)
createGhostSObjectFiles(Name("fieldSet"), forceIgnore)
}
private def indexPath(path: PathLike, forceIgnore: Option[ForceIgnore]): Unit = {
if (DocumentIndex.isExcluded(path))
return
if (path.isDirectory) {
if (forceIgnore.forall(_.includeDirectory(path))) {
path.directoryList() match {
case Left(err) => LoggerOps.error(err)
case Right(parts) => parts.foreach(part => indexPath(path.join(part), forceIgnore))
}
} else {
LoggerOps.debug(LoggerOps.Trace, s"Ignoring directory $path")
}
} else {
// Not testing if this is a regular file to improve scan performance, will fail later on read
if (forceIgnore.forall(_.includeFile(path))) {
val dt = MetadataDocument(path)
dt.foreach(insertDocument)
} else {
LoggerOps.debug(LoggerOps.Trace, s"Ignoring file $path")
}
}
}
private def insertDocument(documentType: MetadataDocument): Unit = {
if (documentType.ignorable)
return
if (documentType.duplicatesAllowed) {
addDocument(documentType)
} else {
// Duplicate detect based on type that will be generated
val typeName = documentType.typeName(namespace)
if (typeNames.contains(typeName)) {
val duplicate = documents(documentType.extension).get(typeName)
logger.log(Issue(ERROR_CATEGORY, LineLocationImpl(documentType.path.toString, 0),
s"File creates duplicate type '$typeName' as '${duplicate.get.head.path}', ignoring"))
} else {
typeNames.add(typeName)
addDocument(documentType)
}
}
}
private def addDocument(docType: MetadataDocument): Unit = {
val extMap = documents.getOrElseUpdate(docType.extension, {mutable.HashMap[TypeName, List[MetadataDocument]]() })
val typeName = docType.typeName(namespace)
extMap.put(typeName, docType :: extMap.getOrElse(typeName, Nil))
}
/** Hack to deal with missing .object-meta.xml files in SFDX */
private def createGhostSObjectFiles(name: Name, forceIgnore: Option[ForceIgnore]): Unit = {
getByExtension(name).foreach(docType => {
val objectDir = docType.path.parent.parent
val metaFile = objectDir.join(objectDir.basename + ".object-meta.xml")
if (!metaFile.isFile) {
if (forceIgnore.forall(_.includeDirectory(metaFile.parent))) {
val objectExt = MetadataDocument.objectExt
val docType = SObjectDocument(metaFile, Name(objectDir.basename))
if (!documents.contains(objectExt) || !documents(objectExt).contains(docType.typeName(namespace))) {
addDocument(docType)
}
}
}
})
}
}
object DocumentIndex {
/** Exclude some paths that we would waste time searching */
def isExcluded(path: PathLike): Boolean = {
val basename = path.basename
if (basename.startsWith(".")) return true
if (basename == "node_modules") return true
false
}
}