diff --git a/src/main/scala/com/normation/cfclerk/domain/TechniqueCategory.scala b/src/main/scala/com/normation/cfclerk/domain/TechniqueCategory.scala index ed38385..e25ae12 100644 --- a/src/main/scala/com/normation/cfclerk/domain/TechniqueCategory.scala +++ b/src/main/scala/com/normation/cfclerk/domain/TechniqueCategory.scala @@ -170,23 +170,23 @@ sealed trait TechniqueCategory { } case class RootTechniqueCategory( - name : String, - description : String, - subCategoryIds: Set[SubTechniqueCategoryId] = Set(), - packageIds : SortedSet[TechniqueId] = SortedSet(), - isSystem : Boolean = false + name : String + , description : String + , subCategoryIds: Set[SubTechniqueCategoryId] = Set() + , packageIds : SortedSet[TechniqueId] = SortedSet() + , isSystem : Boolean = false ) extends TechniqueCategory with HashcodeCaching { type A = RootTechniqueCategoryId.type override lazy val id : A = RootTechniqueCategoryId } case class SubTechniqueCategory( - override val id : SubTechniqueCategoryId, - name : String, - description : String, - subCategoryIds: Set[SubTechniqueCategoryId] = Set(), - packageIds : SortedSet[TechniqueId] = SortedSet(), - isSystem : Boolean = false + override val id : SubTechniqueCategoryId + , name : String + , description : String + , subCategoryIds : Set[SubTechniqueCategoryId] = Set() + , packageIds : SortedSet[TechniqueId] = SortedSet() + , isSystem : Boolean = false ) extends TechniqueCategory with HashcodeCaching { type A = SubTechniqueCategoryId } diff --git a/src/main/scala/com/normation/cfclerk/services/TechniqueLibraryUpdateNotification.scala b/src/main/scala/com/normation/cfclerk/services/TechniqueLibraryUpdateNotification.scala index 258d6e9..a5310c9 100644 --- a/src/main/scala/com/normation/cfclerk/services/TechniqueLibraryUpdateNotification.scala +++ b/src/main/scala/com/normation/cfclerk/services/TechniqueLibraryUpdateNotification.scala @@ -34,9 +34,37 @@ package com.normation.cfclerk.services -import com.normation.cfclerk.domain.TechniqueId +import com.normation.cfclerk.domain.TechniqueName +import com.normation.cfclerk.domain.TechniqueVersion import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId +import com.normation.cfclerk.domain.TechniqueName + + +sealed trait TechniqueVersionModType +case object VersionDeleted extends TechniqueVersionModType +case object VersionAdded extends TechniqueVersionModType +case object VersionUpdated extends TechniqueVersionModType + +sealed trait TechniquesLibraryUpdateType { + val techniqueName: TechniqueName +} + +/** + * All version of a technique were deleted, so the technique is + * full deleted + */ +case class TechniqueDeleted(techniqueName: TechniqueName, deletedVersion: Set[TechniqueVersion]) extends TechniquesLibraryUpdateType + +/** + * Some version of a technique were modified, either: + * - added, + * - deleted, + * - modified. + * + * One version can have only ONE state update. + */ +case class TechniqueUpdated(techniqueName: TechniqueName, diff: Map[TechniqueVersion, TechniqueVersionModType]) extends TechniquesLibraryUpdateType /** * A trait that allows its implementation to get notification @@ -51,7 +79,13 @@ trait TechniquesLibraryUpdateNotification { /** * A name to identify that callback */ - def name:String + def name: String + + /** + * Order: + * higher value for order mean the callback will happen latter + */ + def order: Int /** * That method will be called when techniques are updated. @@ -60,6 +94,6 @@ trait TechniquesLibraryUpdateNotification { * Description is a log description to explain why techniques should be updated * (user action, commit, etc). */ - def updatedTechniques(TechniqueIds:Seq[TechniqueId], modId: ModificationId, actor: EventActor, reason: Option[String]) : Unit + def updatedTechniques(techniqueIds: Map[TechniqueName, TechniquesLibraryUpdateType], modId: ModificationId, actor: EventActor, reason: Option[String]) : Unit -} \ No newline at end of file +} diff --git a/src/main/scala/com/normation/cfclerk/services/TechniqueReader.scala b/src/main/scala/com/normation/cfclerk/services/TechniqueReader.scala index ca8ef0d..6be9657 100644 --- a/src/main/scala/com/normation/cfclerk/services/TechniqueReader.scala +++ b/src/main/scala/com/normation/cfclerk/services/TechniqueReader.scala @@ -48,7 +48,9 @@ case class TechniquesInfo( , techniques: Map[TechniqueName, SortedMap[TechniqueVersion, Technique]] //head of categories is the root category , subCategories: Map[SubTechniqueCategoryId, TechniqueCategory] -) extends HashcodeCaching +) extends HashcodeCaching { + val allCategories = Map[TechniqueCategoryId, TechniqueCategory]() ++ subCategories + (rootCategory.id -> rootCategory) +} //a mutable version of TechniquesInfo, for internal use only ! private[services] class InternalTechniquesInfo( @@ -117,6 +119,6 @@ trait TechniqueReader { * If the sequence is empty, then nothing changed. Else, the list of Technique with * *any* change will be given */ - def getModifiedTechniques : Seq[TechniqueId] + def getModifiedTechniques : Map[TechniqueName, TechniquesLibraryUpdateType] } diff --git a/src/main/scala/com/normation/cfclerk/services/TechniqueRepository.scala b/src/main/scala/com/normation/cfclerk/services/TechniqueRepository.scala index 35e4fce..82f87df 100644 --- a/src/main/scala/com/normation/cfclerk/services/TechniqueRepository.scala +++ b/src/main/scala/com/normation/cfclerk/services/TechniqueRepository.scala @@ -45,10 +45,6 @@ import scala.collection.SortedSet * * */ -/** - * @author vincent - * - */ trait TechniqueRepository { /** @@ -70,6 +66,15 @@ trait TechniqueRepository { // def packageDirectory : File + + /* + * Return the full information about technique. + * That TechniquesInfo data structure is self-consistent by construction, + * so any category reference as a subcategory of an other is + * defined in the map, all techniques in categories is also accessible. etc. + */ + def getTechniquesInfo(): TechniquesInfo + /** * Return all the policies available */ diff --git a/src/main/scala/com/normation/cfclerk/services/UpdateTechniqueLibrary.scala b/src/main/scala/com/normation/cfclerk/services/UpdateTechniqueLibrary.scala index 40d1bf2..712ab6b 100644 --- a/src/main/scala/com/normation/cfclerk/services/UpdateTechniqueLibrary.scala +++ b/src/main/scala/com/normation/cfclerk/services/UpdateTechniqueLibrary.scala @@ -38,6 +38,7 @@ import net.liftweb.common.Box import com.normation.cfclerk.domain.TechniqueId import com.normation.eventlog.EventActor import com.normation.eventlog.ModificationId +import com.normation.cfclerk.domain.TechniqueName /** * A trait that allows to update the reference policy @@ -49,11 +50,11 @@ trait UpdateTechniqueLibrary { * Update the lib, and return the list of * actually updated policy templates. */ - def update(modId: ModificationId, actor:EventActor, reason: Option[String]) : Box[Seq[TechniqueId]] + def update(modId: ModificationId, actor:EventActor, reason: Option[String]) : Box[Map[TechniqueName, TechniquesLibraryUpdateType]] /** * Allows callbacks to be called on a Policy Template library update. */ def registerCallback(callback:TechniquesLibraryUpdateNotification) : Unit -} \ No newline at end of file +} diff --git a/src/main/scala/com/normation/cfclerk/services/impl/FSTechniqueReader.scala b/src/main/scala/com/normation/cfclerk/services/impl/FSTechniqueReader.scala index a8daf58..4fdfaf1 100644 --- a/src/main/scala/com/normation/cfclerk/services/impl/FSTechniqueReader.scala +++ b/src/main/scala/com/normation/cfclerk/services/impl/FSTechniqueReader.scala @@ -107,7 +107,7 @@ class FSTechniqueReader( } //we never know when to read again - override def getModifiedTechniques : Seq[TechniqueId] = Seq() + override def getModifiedTechniques : Map[TechniqueName, TechniquesLibraryUpdateType] = Map() /** * Read the policies @@ -359,4 +359,4 @@ class FSTechniqueReader( doc } -} \ No newline at end of file +} diff --git a/src/main/scala/com/normation/cfclerk/services/impl/GitTechniqueReader.scala b/src/main/scala/com/normation/cfclerk/services/impl/GitTechniqueReader.scala index aeab96e..f3d7ceb 100644 --- a/src/main/scala/com/normation/cfclerk/services/impl/GitTechniqueReader.scala +++ b/src/main/scala/com/normation/cfclerk/services/impl/GitTechniqueReader.scala @@ -71,6 +71,7 @@ import org.eclipse.jgit.diff.DiffEntry import scala.collection.JavaConversions._ import org.eclipse.jgit.diff.DiffFormatter import org.eclipse.jgit.errors.MissingObjectException +import org.eclipse.jgit.diff.DiffEntry.ChangeType /** * @@ -185,9 +186,9 @@ class GitTechniqueReader( private[this] var nextTechniquesInfoCache : (ObjectId,TechniquesInfo) = (revisionProvider.currentRevTreeId, currentTechniquesInfoCache) //a non empty list IS the indicator of differences between current and next - private[this] var modifiedTechniquesCache : Seq[TechniqueId] = Seq() + private[this] var modifiedTechniquesCache : Map[TechniqueName, TechniquesLibraryUpdateType] = Map() - override def getModifiedTechniques : Seq[TechniqueId] = { + override def getModifiedTechniques : Map[TechniqueName, TechniquesLibraryUpdateType] = { val nextId = revisionProvider.getAvailableRevTreeId if(nextId == nextTechniquesInfoCache._1) modifiedTechniquesCache else reader.synchronized { //update next and calculate diffs @@ -199,32 +200,78 @@ class GitTechniqueReader( val diffFmt = new DiffFormatter(null) diffFmt.setRepository(repo.db) - val diffPathEntries : Set[TechniquePath] = + val diffPathEntries : Set[(TechniquePath, ChangeType)] = diffFmt.scan(revisionProvider.currentRevTreeId,nextId).flatMap { diffEntry => - Seq(toTechniquePath(diffEntry.getOldPath), toTechniquePath(diffEntry.getNewPath)) + Seq( (toTechniquePath(diffEntry.getOldPath), diffEntry.getChangeType), (toTechniquePath(diffEntry.getNewPath), diffEntry.getChangeType)) }.toSet diffFmt.release - val modifiedTechniquePath = scala.collection.mutable.Set[TechniquePath]() /* * now, group diff entries by TechniqueId to find which were updated * we take into account any modifications, as anything among a * delete, rename, copy, add, modify must be accepted and the matching * datetime saved. */ - diffPathEntries.foreach { path => - allKnownTechniquePaths.find { TechniquePath => - path.path.startsWith(TechniquePath.path) - }.foreach { TechniquePath => - modifiedTechniquePath += TechniquePath - } //else nothing + val modifiedTechnique : Set[(TechniqueId, ChangeType)] = diffPathEntries.flatMap { case (path, changeType) => + allKnownTechniquePaths.find { techniquePath => + path.path.startsWith(techniquePath.path) + }.map { techniquePath => + val parts = techniquePath.path.split("/") + val id = TechniqueId(TechniqueName(parts(parts.size - 2)), TechniqueVersion(parts(parts.size - 1))) + + (id, changeType) + } } + //now, build the actual modification by techniques: + val techniqueMods = modifiedTechnique.groupBy( _._1.name ).map { case (name, mods) => + //deduplicate mods by ids: + val versionsMods: Map[TechniqueVersion, TechniqueVersionModType] = mods.groupBy( _._1.version).map { case(version, pairs) => + val modTypes = pairs.map( _._2 ).toSet + + //merge logic + //delete + add (+mod) => have to check if still present + //delete (+ mod) => delete + //add (+ mod) => add + //other: mod + if(modTypes.contains(ChangeType.DELETE)) { + if(modTypes.contains(ChangeType.ADD)) { + if(currentTechniquesInfoCache.techniquesCategory.isDefinedAt(TechniqueId(name, version))) { + //it was present before mod, so can't have been added first, so + //it was deleted then added (certainly a move), so it is actually mod + (version, VersionUpdated) + } else { + //it was not here, so was added then deleted, so it's a noop. + //not sure we want to trace that ? As a deletion perhaps ? + (version, VersionDeleted) + } + } else { + (version, VersionDeleted) + } + } else if(modTypes.contains(ChangeType.ADD)) { + //add + (version, VersionAdded) + } else { + //mod + (version, VersionUpdated) + } + }.toMap + + //now, build the technique mod. + //distinguish the case where all mod are deletion AND they represent the whole set of known version + //from other case (just simple updates) + + if(versionsMods.values.forall( _ == VersionDeleted) + && currentTechniquesInfoCache.techniques.get(name).map( _.size) == Some(versionsMods.size) + ) { + (name, TechniqueDeleted(name, versionsMods.keySet)) + } else { + (name, TechniqueUpdated(name, versionsMods)) + } + }.toMap + //Ok, now rebuild Technique ! - modifiedTechniquesCache = modifiedTechniquePath.map { s => - val parts = s.path.split("/") - TechniqueId(TechniqueName(parts(parts.size - 2)), TechniqueVersion(parts(parts.size - 1))) - }.toSeq + modifiedTechniquesCache = techniqueMods nextTechniquesInfoCache = (nextId, nextTechniquesInfo) modifiedTechniquesCache } @@ -362,7 +409,7 @@ class GitTechniqueReader( if(modifiedTechniquesCache.nonEmpty) { currentTechniquesInfoCache = nextTechniquesInfoCache._2 revisionProvider.setCurrentRevTreeId(nextTechniquesInfoCache._1) - modifiedTechniquesCache = Seq() + modifiedTechniquesCache = Map() } currentTechniquesInfoCache } diff --git a/src/main/scala/com/normation/cfclerk/services/impl/TechniqueRepositoryImpl.scala b/src/main/scala/com/normation/cfclerk/services/impl/TechniqueRepositoryImpl.scala index b8aed33..ccda9db 100644 --- a/src/main/scala/com/normation/cfclerk/services/impl/TechniqueRepositoryImpl.scala +++ b/src/main/scala/com/normation/cfclerk/services/impl/TechniqueRepositoryImpl.scala @@ -56,7 +56,7 @@ class TechniqueRepositoryImpl( /** * Callback to call on PTLib update */ - private[this] val callbacks = scala.collection.mutable.Buffer(refLibCallbacks:_*) + private[this] var callbacks = refLibCallbacks.sortBy( _.order ) /* @@ -86,19 +86,27 @@ class TechniqueRepositoryImpl( /** - * Register a new callback + * Register a new callback with a order. + * Sort by order after each registration */ override def registerCallback(callback:TechniquesLibraryUpdateNotification) : Unit = { - callbacks.append(callback) + callbacks = (callbacks :+ callback).sortBy( _.order ) } - override def update(modId: ModificationId, actor:EventActor, reason: Option[String]) : Box[Seq[TechniqueId]] = { + override def update(modId: ModificationId, actor:EventActor, reason: Option[String]) : Box[Map[TechniqueName, TechniquesLibraryUpdateType]] = { try { val modifiedPackages = techniqueReader.getModifiedTechniques if (modifiedPackages.nonEmpty || /* first time init */ null == techniqueInfosCache) { logger.info("Reloading technique library, " + { if (modifiedPackages.isEmpty) "no modified techniques found" - else "found modified technique(s): " + modifiedPackages.mkString(", ") + else { + val details = modifiedPackages.values.map { + case TechniqueDeleted(name, versions) => s"['${name}': deleted (${versions.mkString(", ")})]" + case TechniqueUpdated(name, mods) => s"['${name}': updated (${mods.map(x => s"${x._1}: ${x._2}").mkString(", ")})]" + } + + "found modified technique(s): " + details.mkString(", ") + } }) techniqueInfosCache = techniqueReader.readTechniques @@ -142,6 +150,8 @@ class TechniqueRepositoryImpl( }).toMap } + override def getTechniquesInfo() = techniqueInfosCache + override def getTechniqueVersions(name: TechniqueName): SortedSet[TechniqueVersion] = { SortedSet[TechniqueVersion]() ++ techniqueInfosCache.techniques.get(name).toSeq.flatMap(_.keySet) } @@ -174,7 +184,11 @@ class TechniqueRepositoryImpl( } override def getLastTechniqueByName(policyName: TechniqueName): Option[Technique] = { - techniqueInfosCache.techniques.get(policyName).map { versions => versions.last._2 } + for { + versions <- techniqueInfosCache.techniques.get(policyName) + } yield { + versions.last._2 + } } //////////////////////////////////// categories ///////////////////////////// diff --git a/src/test/scala/com/normation/cfclerk/services/DummyTechniqueReader.scala b/src/test/scala/com/normation/cfclerk/services/DummyTechniqueReader.scala index fcd008d..c269ed8 100644 --- a/src/test/scala/com/normation/cfclerk/services/DummyTechniqueReader.scala +++ b/src/test/scala/com/normation/cfclerk/services/DummyTechniqueReader.scala @@ -75,6 +75,6 @@ class DummyTechniqueReader(policies:Seq[Technique]=Seq(Technique(TechniqueId(Tec def getMetadataContent[T](techniqueId: TechniqueId)(useIt : Option[InputStream] => T) : T = useIt(None) def getReportingDetailsContent[T](techniqueId: TechniqueId)(useIt : Option[InputStream] => T) : T = useIt(None) def getTemplateContent[T](templateName: Cf3PromisesFileTemplateId)(useIt : Option[InputStream] => T) : T = useIt(None) - def getModifiedTechniques : Seq[TechniqueId] = Seq() + def getModifiedTechniques : Map[TechniqueName, TechniquesLibraryUpdateType] = Map() } diff --git a/src/test/scala/com/normation/cfclerk/services/DummyTechniqueRepository.scala b/src/test/scala/com/normation/cfclerk/services/DummyTechniqueRepository.scala index 986e7cf..2ec1a3b 100644 --- a/src/test/scala/com/normation/cfclerk/services/DummyTechniqueRepository.scala +++ b/src/test/scala/com/normation/cfclerk/services/DummyTechniqueRepository.scala @@ -87,21 +87,8 @@ class DummyTechniqueRepository(policies: Seq[Technique] = Seq()) extends Techniq def getByName(policyName: TechniqueName) = ??? -// def getVariables(policyName: TechniqueId, includeSystemVar: Boolean = false): Seq[VariableSpec] = -// policyName.name.value match { -// case "policy1" => -// List(InputVariableSpec("$variable1", "a variable1")) -// case "policy2" => -// val variable = InputVariableSpec("$variable2", "a variable2", multivalued = true) -// List(variable, InputVariableSpec("$variable22", "a variable22")) -// case "policy3" => -// List(InputVariableSpec("$variable3", "a variable3")) -// case "policy4" => -// List(InputVariableSpec("$variable4", "an variable4")) -// case "foo" => -// List(InputVariableSpec("$bar", "bar")) -// } -// + def getTechniquesInfo = ??? + override def getTechniqueVersions(name:TechniqueName) : SortedSet[TechniqueVersion] = SortedSet.empty[TechniqueVersion] def manageDependencies(chosenTemplate: Seq[Cf3PromisesFileTemplateId] , includeExternalDependencies : Boolean = true) : Seq[Cf3PromisesFileTemplateId] = { @@ -114,4 +101,4 @@ class DummyTechniqueRepository(policies: Seq[Technique] = Seq()) extends Techniq def getParents_TechniqueCategory(id: TechniqueCategoryId): Box[List[TechniqueCategory]] = null def getParentTechniqueCategory_forTechnique(id: TechniqueId): Box[TechniqueCategory] = null -} \ No newline at end of file +}