Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
Copyright (c) 2021 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.
*/
package com.nawforce.pkgforce.stream
import com.nawforce.pkgforce.diagnostics._
import com.nawforce.pkgforce.documents._
import com.nawforce.pkgforce.names.{DotName, Name}
import com.nawforce.pkgforce.path.{Location, PathLike, PathLocation}
import com.nawforce.pkgforce.xml.{XMLDocumentLike, XMLElementLike, XMLException, XMLFactory}
import com.nawforce.runtime.xml.XMLDocument
import scala.collection.compat.immutable.ArraySeq
import scala.collection.mutable
sealed abstract class SharingModel(val value: String)
case object PrivateSharingModel extends SharingModel("Private")
case object ReadSharingModel extends SharingModel("Read")
case object ReadWriteSharingModel extends SharingModel("ReadWrite")
case object ReadWriteTransferSharingModel extends SharingModel("ReadWriteTransfer")
case object FullAccessSharingModel extends SharingModel("FullAccess")
case object ControlledByParentSharingModel extends SharingModel("ControlledByParent")
case object ControlledByCampaignSharingModel extends SharingModel("ControlledByCampaign")
case object ControlledByLeadOrContractSharingModel
extends SharingModel("ControlledByLeadOrContract")
sealed trait CustomSettingType
case object ListCustomSetting extends CustomSettingType
case object HierarchyCustomSetting extends CustomSettingType
final case class SObjectEvent(
sourceInfo: Option[SourceInfo],
name: Name,
isDefining: Boolean, // Metadata is defining a new SObject
customSettingsType: Option[CustomSettingType],
sharingModel: Option[SharingModel]
) extends PackageEvent
final case class CustomFieldEvent(
sourceInfo: SourceInfo,
name: Name,
rawType: Name,
referenceTo: Option[(Name, Name)], // For lookups like fields (referenceTo, relationshipName)
relatedField: Option[(Name, Name)] // For summary fields (sobject, field)
) extends PackageEvent
final case class FieldsetEvent(sourceInfo: SourceInfo, name: Name) extends PackageEvent
final case class SharingReasonEvent(sourceInfo: SourceInfo, name: Name) extends PackageEvent
/** Convert SObject documents/folders into PackageEvents. We must call this even if there is not
* object-meta.xml file present to collect the SFDX fields, fieldSets and sharingRules.
*/
object SObjectGenerator {
def iterator(index: DocumentIndex): Iterator[PackageEvent] = {
// Convert SObjectLike things to events
val sObjectEvents: mutable.Map[Name, Array[PackageEvent]] =
index
.get(SObjectNature)
.map(docInfo =>
(Name(docInfo._1), toEvents(docInfo._2.flatMap(MetadataDocument(_))).toArray)
)
.to(mutable.Map)
// SObjects need ordering so lookup target is output before the object using lookup
val emitted = new mutable.HashSet[Name]()
val output = new mutable.ArrayBuffer[PackageEvent]()
var found = true
while (found && sObjectEvents.nonEmpty) {
found = false
sObjectEvents.foreach(kv => {
val depends = kv._2
.collect { case CustomFieldEvent(_, _, _, Some((referenceTo, _)), _) =>
Name(s"schema.$referenceTo")
}
.filter(sObjectEvents.contains)
if (depends.forall(d => emitted.contains(d))) {
sObjectEvents.remove(kv._1)
emitted.add(kv._1)
output.appendAll(kv._2)
found = true
}
})
}
// If ordering failed, apply any left to end, this will fail on deploy
sObjectEvents.foreach(kv => output.appendAll(kv._2))
output.iterator
}
def toEvents(documents: List[MetadataDocument]): Iterator[PackageEvent] = {
// Parse controlling doc, if we have one
val controllingDoc = documents.find(_.nature == SObjectNature)
val controllingPath = controllingDoc.map(_.path)
val sourceData = controllingPath.flatMap(_.readSourceData().toOption)
val sourceInfo =
sourceData.map(source => SourceInfo(PathLocation(controllingPath.get, Location.all), source))
val parsed = sourceData.map(source => XMLDocument(controllingPath.get, source))
if (parsed.nonEmpty && parsed.get.issues.nonEmpty)
return IssuesEvent.iterator(parsed.get.issues)
// Extract some needed info
val controllingContent = parsed.flatMap(_.value)
val customSettingsType =
controllingContent
.map(content => extractCustomSettingsType(content))
.getOrElse(IssuesAnd(None))
val sharingModelType =
controllingContent.map(content => extractSharingModel(content)).getOrElse(IssuesAnd(None))
val isDefining =
controllingContent.exists(content => content.rootElement.getChildren("label").nonEmpty)
// Collect whatever we can find into the stream, this is deliberately lax we are not trying to find errors here
Iterator(
SObjectEvent(
sourceInfo,
getName(controllingPath, documents),
isDefining,
customSettingsType.value,
sharingModelType.value
)
) ++
IssuesEvent.iterator(customSettingsType.issues) ++
IssuesEvent.iterator(sharingModelType.issues) ++
controllingContent
.map(content => {
val rootElement = content.rootElement
rootElement
.getChildren("fields")
.flatMap(field => {
createField(
SourceInfo(PathLocation(controllingPath.get, Location(field.line)), sourceData.get),
field,
controllingPath.get
)
}) ++
rootElement
.getChildren("fieldSets")
.flatMap(fieldSet => {
createFieldSet(
SourceInfo(
PathLocation(controllingPath.get, Location(fieldSet.line)),
sourceData.get
),
fieldSet,
controllingPath.get
)
}) ++
rootElement
.getChildren("sharingReasons")
.flatMap(sharingReason => {
createSharingReason(
SourceInfo(
PathLocation(controllingPath.get, Location(sharingReason.line)),
sourceData.get
),
sharingReason,
controllingPath.get
)
})
})
.getOrElse(Iterator()) ++
collectMetadata(documents, FieldNature, "CustomField", createField).iterator ++
collectMetadata(documents, FieldSetNature, "FieldSet", createFieldSet).iterator ++
collectMetadata(documents, SharingReasonNature, "SharingReason", createSharingReason).iterator
}
private def getName(
controllingPath: Option[PathLike],
documents: List[MetadataDocument]
): Name = {
// Either .object/.object-meta-xml or component in subdirectory
Name(
controllingPath
.map(path => {
if (path.toString.endsWith(".object")) {
path.basename.substring(0, path.basename.length - 7)
} else {
path.parent.basename
}
})
.getOrElse(documents.head.path.parent.parent.basename)
)
}
private def extractCustomSettingsType(
doc: XMLDocumentLike
): IssuesAnd[Option[CustomSettingType]] = {
doc.rootElement.getOptionalSingleChildAsString("customSettingsType") match {
case Some("List") => IssuesAnd(Some(ListCustomSetting))
case Some("Hierarchy") => IssuesAnd(Some(HierarchyCustomSetting))
case Some(x) =>
IssuesAnd(
ArraySeq(
Issue(
ERROR_CATEGORY,
PathLocation(doc.path, Location(doc.rootElement.line)),
s"Unexpected customSettingsType value '$x', should be 'List' or 'Hierarchy'"
)
),
None
)
case None => IssuesAnd(None)
}
}
private val allSharingModels = Seq(
PrivateSharingModel,
ReadSharingModel,
ReadWriteSharingModel,
ReadWriteTransferSharingModel,
FullAccessSharingModel,
ControlledByParentSharingModel,
ControlledByCampaignSharingModel,
ControlledByLeadOrContractSharingModel
)
private def extractSharingModel(doc: XMLDocumentLike): IssuesAnd[Option[SharingModel]] = {
val sharingModel = doc.rootElement.getOptionalSingleChildAsString("sharingModel")
if (sharingModel.nonEmpty) {
val matched = allSharingModels.find(_.value == sharingModel.get)
if (matched.nonEmpty) {
IssuesAnd(matched)
} else {
IssuesAnd(
ArraySeq(
Issue(
ERROR_CATEGORY,
PathLocation(doc.path, Location(doc.rootElement.line)),
s"Unexpected sharingModel value '${sharingModel.get}'"
)
),
None
)
}
} else {
IssuesAnd(None)
}
}
private def createField(
sourceInfo: SourceInfo,
elem: XMLElementLike,
path: PathLike
): Iterator[PackageEvent] = {
catchXMLExceptions(path) {
val name = Name(elem.getSingleChildAsString("fullName").trim)
// We only need custom fields
if (!name.toString.endsWith("__c"))
return Iterator()
val rawType = elem.getSingleChildAsString("type").trim
if (!fieldTypes.contains(rawType)) {
return IssuesEvent.iterator(
ArraySeq(
Issue(
path,
Diagnostic(
ERROR_CATEGORY,
Location(elem.line),
s"Unrecognised type '$rawType' on custom field '$name'"
)
)
)
)
}
// Create additional fields & lookup relationships for special fields
val target = rawType match {
case "Lookup" | "MasterDetail" | "MetadataRelationship" =>
Some(
(
Name(elem.getSingleChildAsString("referenceTo").trim),
Name(elem.getSingleChildAsString("relationshipName").trim)
)
)
case _ => None
}
// Child relationship field references
val related = rawType match {
case "Summary" =>
elem
.getOptionalSingleChildAsString("summarizedField")
.map(fieldStr => DotName(fieldStr.trim))
.map(field => (field.firstName, field.lastName))
case _ => None
}
Iterator(CustomFieldEvent(sourceInfo, name, Name(rawType), target, related))
}
}
private def createFieldSet(
sourceInfo: SourceInfo,
elem: XMLElementLike,
path: PathLike
): Iterator[PackageEvent] = {
catchXMLExceptions(path) {
Iterator(FieldsetEvent(sourceInfo, Name(elem.getSingleChildAsString("fullName"))))
}
}
private def createSharingReason(
sourceInfo: SourceInfo,
elem: XMLElementLike,
path: PathLike
): Iterator[PackageEvent] = {
catchXMLExceptions(path) {
Iterator(SharingReasonEvent(sourceInfo, Name(elem.getSingleChildAsString("fullName"))))
}
}
private def collectMetadata(
docs: List[MetadataDocument],
nature: MetadataNature,
rootElement: String,
op: (SourceInfo, XMLElementLike, PathLike) => Iterator[PackageEvent]
): List[PackageEvent] = {
docs
.filter(_.nature == nature)
.flatMap(doc => {
catchXMLExceptions(doc.path) {
doc.path.readSourceData() match {
case Left(err) =>
IssuesEvent
.iterator(ArraySeq(Issue(doc.path, Diagnostic(ERROR_CATEGORY, Location(0), err))))
case Right(sourceData) =>
XMLFactory.parse(doc.path) match {
case IssuesAnd(issues, content) if content.isEmpty => IssuesEvent.iterator(issues)
case IssuesAnd(_, content) =>
content.get.rootElement.checkIsOrThrow(rootElement)
op(
SourceInfo(PathLocation(doc.path, Location.all), sourceData),
content.get.rootElement,
doc.path
)
}
}
}
})
}
private def catchXMLExceptions(
path: PathLike
)(op: => Iterator[PackageEvent]): Iterator[PackageEvent] = {
try {
op
} catch {
case e: XMLException =>
IssuesEvent.iterator(ArraySeq(Issue(path, Diagnostic(ERROR_CATEGORY, e.where, e.msg))))
}
}
private val fieldTypes = Set[String](
"MasterDetail",
"Lookup",
"MetadataRelationship",
"AutoNumber",
"Checkbox",
"Currency",
"Date",
"DateTime",
"Email",
"EncryptedText",
"Number",
"Percent",
"Phone",
"Picklist",
"MultiselectPicklist",
"Summary",
"Text",
"TextArea",
"LongTextArea",
"Url",
"File",
"Location",
"Time",
"Html"
)
}