1<?php
2/**
3 * Copyright (c) 2010-2018 Combodo SARL
4 *
5 * This file is part of iTop.
6 *
7 * iTop is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * iTop is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU Affero General Public License for more details.
16 *
17 * You should have received a copy of the GNU Affero General Public License
18 * along with iTop. If not, see <http://www.gnu.org/licenses/>
19 *
20 */
21
22/**
23 * Created by PhpStorm.
24 * Date: 24/08/2018
25 * Time: 14:35
26 */
27class ormSet
28{
29	protected $sClass; // class of the field
30	protected $sAttCode; // attcode of the field
31	protected $aOriginalObjects = null;
32
33	/**
34	 * Object from the original set, minus the removed objects
35	 */
36	protected $aPreserved = array();
37
38	/**
39	 * New items
40	 */
41	protected $aAdded = array();
42
43	/**
44	 * Removed items
45	 */
46	protected $aRemoved = array();
47
48	/**
49	 * Modified items (mass edit)
50	 */
51	protected $aModified = array();
52
53	/**
54	 * @var int Max number of tags in collection
55	 */
56	protected $iLimit;
57
58	/**
59	 * __toString magical function overload.
60	 */
61	public function __toString()
62	{
63		$aValue = $this->GetValues();
64		if (!empty($aValue))
65		{
66			return implode(', ', $aValue);
67		}
68		else
69		{
70			return ' ';
71		}
72	}
73
74	/**
75	 * ormSet constructor.
76	 *
77	 * @param string $sClass
78	 * @param string $sAttCode
79	 * @param int $iLimit
80	 *
81	 * @throws \Exception
82	 */
83	public function __construct($sClass, $sAttCode, $iLimit = 12)
84	{
85		$this->sAttCode = $sAttCode;
86
87		$oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode);
88		if (!$oAttDef instanceof AttributeSet)
89		{
90			throw new Exception("ormSet: field {$sClass}:{$sAttCode} is not a set");
91		}
92		$this->sClass = $sClass;
93		$this->iLimit = $iLimit;
94	}
95
96	/**
97	 * @return string
98	 */
99	public function GetClass()
100	{
101		return $this->sClass;
102	}
103
104	/**
105	 * @return string
106	 */
107	public function GetAttCode()
108	{
109		return $this->sAttCode;
110	}
111
112	/**
113	 *
114	 * @param array $aItems
115	 *
116	 * @throws \CoreException
117	 * @throws \CoreUnexpectedValue when a code is invalid
118	 */
119	public function SetValues($aItems)
120	{
121		if (!is_array($aItems))
122		{
123			throw new CoreUnexpectedValue("Wrong value {$aItems} for {$this->sClass}:{$this->sAttCode}");
124		}
125
126		$aValues = array();
127		$iCount = 0;
128		$bError = false;
129		foreach($aItems as $oItem)
130		{
131			$iCount++;
132			if (($this->iLimit != 0) && ($iCount > $this->iLimit))
133			{
134				$bError = true;
135				continue;
136			}
137			$aValues[] = $oItem;
138		}
139
140		$this->aPreserved = &$aValues;
141		$this->aRemoved = array();
142		$this->aAdded = array();
143		$this->aModified = array();
144		$this->aOriginalObjects = $aValues;
145
146		if ($bError)
147		{
148			throw new CoreException("Maximum number of items ({$this->iLimit}) reached for {$this->sClass}:{$this->sAttCode}");
149		}
150	}
151
152	public function Count()
153	{
154		return count($this->aPreserved) + count($this->aAdded) - count($this->aRemoved);
155	}
156
157	/**
158	 * @return array of codes
159	 */
160	public function GetValues()
161	{
162		$aValues = array_merge($this->aPreserved, $this->aAdded);
163
164		return $aValues;
165	}
166
167	/**
168	 * @return array of tag labels indexed by code for only the added tags
169	 */
170	private function GetAdded()
171	{
172		return $this->aAdded;
173	}
174
175	/**
176	 * @return array of tag labels indexed by code for only the removed tags
177	 */
178	private function GetRemoved()
179	{
180		return $this->aRemoved;
181	}
182
183	/** Get the delta with another ItemSet
184	 *
185	 *  $aDelta['added] = array of tag codes for only the added tags
186	 *  $aDelta['removed'] = array of tag codes for only the removed tags
187	 *
188	 * @param \ormSet $oOtherSet
189	 *
190	 * @return array
191	 *
192	 * @throws \CoreException
193	 * @throws \CoreUnexpectedValue
194	 * @throws \Exception
195	 */
196	public function GetDelta(ormSet $oOtherSet)
197	{
198		$oSet = new ormSet($this->sClass, $this->sAttCode, $this->iLimit);
199		// Set the initial value
200		$aOrigItems = $this->GetValues();
201		$oSet->SetValues($aOrigItems);
202
203		// now remove everything
204		foreach($aOrigItems as $oItem)
205		{
206			$oSet->Remove($oItem);
207		}
208
209		// now add the tags of the other ItemSet
210		foreach($oOtherSet->GetValues() as $oItem)
211		{
212			$oSet->Add($oItem);
213		}
214
215		$aDelta = array();
216		$aDelta['added'] = $oSet->GetAdded();
217		$aDelta['removed'] = $oSet->GetRemoved();
218
219		return $aDelta;
220	}
221
222	/**
223	 * @return string[] list of codes for partial entries
224	 */
225	public function GetModified()
226	{
227		return $this->aModified;
228	}
229
230	/**
231	 * Apply a delta to the current ItemSet
232	 *  $aDelta['added] = array of added items
233	 *  $aDelta['removed'] = array of removed items
234	 *
235	 * @param $aDelta
236	 *
237	 * @throws \CoreException
238	 */
239	public function ApplyDelta($aDelta)
240	{
241		if (isset($aDelta['removed']))
242		{
243			foreach($aDelta['removed'] as $oItem)
244			{
245				$this->Remove($oItem);
246			}
247		}
248		if (isset($aDelta['added']))
249		{
250			foreach($aDelta['added'] as $oItem)
251			{
252				$this->Add($oItem);
253			}
254		}
255
256		// Reset the object
257		$this->SetValues($this->GetValues());
258	}
259
260	/**
261	 * @param string $oItem
262	 *
263	 * @throws \CoreException
264	 */
265	public function Add($oItem)
266	{
267		if (($this->iLimit != 0) && ($this->Count() > $this->iLimit))
268		{
269			throw new CoreException("Maximum number of items ({$this->iLimit}) reached for {$this->sClass}:{$this->sAttCode}");
270		}
271		if ($this->IsItemInList($this->aPreserved, $oItem) || $this->IsItemInList($this->aAdded, $oItem))
272		{
273			// nothing to do, already existing tag
274			return;
275		}
276		// if removed and added again
277		if (($this->RemoveItemFromList($this->aRemoved, $oItem)) !== false)
278		{
279			// put it back into preserved
280			$this->aPreserved[] = $oItem;
281			// no need to add it to aModified : was already done when calling RemoveItem method
282		}
283		else
284		{
285			$this->aAdded[] = $oItem;
286			$this->aModified[] = $oItem;
287		}
288	}
289
290	/**
291	 * @param $oItem
292	 */
293	public function Remove($oItem)
294	{
295		if ($this->IsItemInList($this->aRemoved, $oItem))
296		{
297			// nothing to do, already removed tag
298			return;
299		}
300
301		if ($this->RemoveItemFromList($this->aAdded, $oItem) !== false)
302		{
303			$this->aModified[] = $oItem;
304
305			return; // if present in added, can't be in preserved !
306		}
307
308		if ($this->RemoveItemFromList($this->aPreserved, $oItem) !== false)
309		{
310			$this->aModified[] = $oItem;
311			$this->aRemoved[] = $oItem;
312		}
313	}
314
315	private function IsItemInList($aItemList, $oItem)
316	{
317		return in_array($oItem, $aItemList);
318	}
319
320	/**
321	 * @param \DBObject[] $aItemList
322	 * @param $oItem
323	 *
324	 * @return bool|\DBObject false if not found, else the removed element
325	 */
326	private function RemoveItemFromList(&$aItemList, $oItem)
327	{
328		if (!($this->IsItemInList($aItemList, $oItem)))
329		{
330			return false;
331		}
332		foreach ($aItemList as $index => $value)
333		{
334			if ($value === $oItem)
335			{
336				unset($aItemList[$index]);
337				return $oItem;
338			}
339		}
340
341		return false;
342	}
343
344	/**
345	 * Populates the added and removed arrays for bulk edit
346	 *
347	 * @param string[] $aItems
348	 *
349	 * @throws \CoreException
350	 */
351	public function GenerateDiffFromArray($aItems)
352	{
353		foreach($this->GetValues() as $oCurrentItem)
354		{
355			if (!in_array($oCurrentItem, $aItems))
356			{
357				$this->Remove($oCurrentItem);
358			}
359		}
360
361		foreach($aItems as $oNewItem)
362		{
363			$this->Add($oNewItem);
364		}
365	}
366
367	/**
368	 * Compare Item Set
369	 *
370	 * @param \ormSet $other
371	 *
372	 * @return bool true if same tag set
373	 */
374	public function Equals(ormSet $other)
375	{
376		return implode(', ', $this->GetValue()) === implode(', ', $other->GetValue());
377	}
378
379
380}