001    package org.bukkit.inventory;
002    
003    import com.google.common.collect.ImmutableMap;
004    import java.util.LinkedHashMap;
005    import java.util.Map;
006    
007    import org.apache.commons.lang.Validate;
008    import org.bukkit.Bukkit;
009    import org.bukkit.Material;
010    import org.bukkit.Utility;
011    import org.bukkit.configuration.serialization.ConfigurationSerializable;
012    import org.bukkit.enchantments.Enchantment;
013    import org.bukkit.inventory.meta.ItemMeta;
014    import org.bukkit.material.MaterialData;
015    
016    /**
017     * Represents a stack of items
018     */
019    public class ItemStack implements Cloneable, ConfigurationSerializable {
020        private int type = 0;
021        private int amount = 0;
022        private MaterialData data = null;
023        private short durability = 0;
024        private ItemMeta meta;
025    
026        @Utility
027        protected ItemStack() {}
028    
029        /**
030         * Defaults stack size to 1, with no extra data
031         *
032         * @param type item material id
033         * @deprecated Magic value
034         */
035        @Deprecated
036        public ItemStack(final int type) {
037            this(type, 1);
038        }
039    
040        /**
041         * Defaults stack size to 1, with no extra data
042         *
043         * @param type item material
044         */
045        public ItemStack(final Material type) {
046            this(type, 1);
047        }
048    
049        /**
050         * An item stack with no extra data
051         *
052         * @param type item material id
053         * @param amount stack size
054         * @deprecated Magic value
055         */
056        @Deprecated
057        public ItemStack(final int type, final int amount) {
058            this(type, amount, (short) 0);
059        }
060    
061        /**
062         * An item stack with no extra data
063         *
064         * @param type item material
065         * @param amount stack size
066         */
067        public ItemStack(final Material type, final int amount) {
068            this(type.getId(), amount);
069        }
070    
071        /**
072         * An item stack with the specified damage / durability
073         *
074         * @param type item material id
075         * @param amount stack size
076         * @param damage durability / damage
077         * @deprecated Magic value
078         */
079        @Deprecated
080        public ItemStack(final int type, final int amount, final short damage) {
081            this.type = type;
082            this.amount = amount;
083            this.durability = damage;
084        }
085    
086        /**
087         * An item stack with the specified damage / durabiltiy
088         *
089         * @param type item material
090         * @param amount stack size
091         * @param damage durability / damage
092         */
093        public ItemStack(final Material type, final int amount, final short damage) {
094            this(type.getId(), amount, damage);
095        }
096    
097        /**
098         * @deprecated this method uses an ambiguous data byte object
099         */
100        @Deprecated
101        public ItemStack(final int type, final int amount, final short damage, final Byte data) {
102            this.type = type;
103            this.amount = amount;
104            this.durability = damage;
105            if (data != null) {
106                createData(data);
107                this.durability = data;
108            }
109        }
110    
111        /**
112         * @deprecated this method uses an ambiguous data byte object
113         */
114        @Deprecated
115        public ItemStack(final Material type, final int amount, final short damage, final Byte data) {
116            this(type.getId(), amount, damage, data);
117        }
118    
119        /**
120         * Creates a new item stack derived from the specified stack
121         *
122         * @param stack the stack to copy
123         * @throws IllegalArgumentException if the specified stack is null or
124         *     returns an item meta not created by the item factory
125         */
126        public ItemStack(final ItemStack stack) throws IllegalArgumentException {
127            Validate.notNull(stack, "Cannot copy null stack");
128            this.type = stack.getTypeId();
129            this.amount = stack.getAmount();
130            this.durability = stack.getDurability();
131            this.data = stack.getData();
132            if (stack.hasItemMeta()) {
133                setItemMeta0(stack.getItemMeta(), getType0());
134            }
135        }
136    
137        /**
138         * Gets the type of this item
139         *
140         * @return Type of the items in this stack
141         */
142        @Utility
143        public Material getType() {
144            return getType0(getTypeId());
145        }
146    
147        private Material getType0() {
148            return getType0(this.type);
149        }
150    
151        private static Material getType0(int id) {
152            Material material = Material.getMaterial(id);
153            return material == null ? Material.AIR : material;
154        }
155    
156        /**
157         * Sets the type of this item
158         * <p>
159         * Note that in doing so you will reset the MaterialData for this stack
160         *
161         * @param type New type to set the items in this stack to
162         */
163        @Utility
164        public void setType(Material type) {
165            Validate.notNull(type, "Material cannot be null");
166            setTypeId(type.getId());
167        }
168    
169        /**
170         * Gets the type id of this item
171         *
172         * @return Type Id of the items in this stack
173         * @deprecated Magic value
174         */
175        @Deprecated
176        public int getTypeId() {
177            return type;
178        }
179    
180        /**
181         * Sets the type id of this item
182         * <p>
183         * Note that in doing so you will reset the MaterialData for this stack
184         *
185         * @param type New type id to set the items in this stack to
186         * @deprecated Magic value
187         */
188        @Deprecated
189        public void setTypeId(int type) {
190            this.type = type;
191            if (this.meta != null) {
192                this.meta = Bukkit.getItemFactory().asMetaFor(meta, getType0());
193            }
194            createData((byte) 0);
195        }
196    
197        /**
198         * Gets the amount of items in this stack
199         *
200         * @return Amount of items in this stick
201         */
202        public int getAmount() {
203            return amount;
204        }
205    
206        /**
207         * Sets the amount of items in this stack
208         *
209         * @param amount New amount of items in this stack
210         */
211        public void setAmount(int amount) {
212            this.amount = amount;
213        }
214    
215        /**
216         * Gets the MaterialData for this stack of items
217         *
218         * @return MaterialData for this item
219         */
220        public MaterialData getData() {
221            Material mat = getType();
222            if (data == null && mat != null && mat.getData() != null) {
223                data = mat.getNewData((byte) this.getDurability());
224            }
225    
226            return data;
227        }
228    
229        /**
230         * Sets the MaterialData for this stack of items
231         *
232         * @param data New MaterialData for this item
233         */
234        public void setData(MaterialData data) {
235            Material mat = getType();
236    
237            if (data == null || mat == null || mat.getData() == null) {
238                this.data = data;
239            } else {
240                if ((data.getClass() == mat.getData()) || (data.getClass() == MaterialData.class)) {
241                    this.data = data;
242                } else {
243                    throw new IllegalArgumentException("Provided data is not of type " + mat.getData().getName() + ", found " + data.getClass().getName());
244                }
245            }
246        }
247    
248        /**
249         * Sets the durability of this item
250         *
251         * @param durability Durability of this item
252         */
253        public void setDurability(final short durability) {
254            this.durability = durability;
255        }
256    
257        /**
258         * Gets the durability of this item
259         *
260         * @return Durability of this item
261         */
262        public short getDurability() {
263            return durability;
264        }
265    
266        /**
267         * Get the maximum stacksize for the material hold in this ItemStack.
268         * (Returns -1 if it has no idea)
269         *
270         * @return The maximum you can stack this material to.
271         */
272        @Utility
273        public int getMaxStackSize() {
274            Material material = getType();
275            if (material != null) {
276                return material.getMaxStackSize();
277            }
278            return -1;
279        }
280    
281        private void createData(final byte data) {
282            Material mat = Material.getMaterial(type);
283    
284            if (mat == null) {
285                this.data = new MaterialData(type, data);
286            } else {
287                this.data = mat.getNewData(data);
288            }
289        }
290    
291        @Override
292        @Utility
293        public String toString() {
294            StringBuilder toString = new StringBuilder("ItemStack{").append(getType().name()).append(" x ").append(getAmount());
295            if (hasItemMeta()) {
296                toString.append(", ").append(getItemMeta());
297            }
298            return toString.append('}').toString();
299        }
300    
301        @Override
302        @Utility
303        public boolean equals(Object obj) {
304            if (this == obj) {
305                return true;
306            }
307            if (!(obj instanceof ItemStack)) {
308                return false;
309            }
310    
311            ItemStack stack = (ItemStack) obj;
312            return getAmount() == stack.getAmount() && isSimilar(stack);
313        }
314    
315        /**
316         * This method is the same as equals, but does not consider stack size
317         * (amount).
318         *
319         * @param stack the item stack to compare to
320         * @return true if the two stacks are equal, ignoring the amount
321         */
322        @Utility
323        public boolean isSimilar(ItemStack stack) {
324            if (stack == null) {
325                return false;
326            }
327            if (stack == this) {
328                return true;
329            }
330            return getTypeId() == stack.getTypeId() && getDurability() == stack.getDurability() && hasItemMeta() == stack.hasItemMeta() && (hasItemMeta() ? Bukkit.getItemFactory().equals(getItemMeta(), stack.getItemMeta()) : true);
331        }
332    
333        @Override
334        public ItemStack clone() {
335            try {
336                ItemStack itemStack = (ItemStack) super.clone();
337    
338                if (this.meta != null) {
339                    itemStack.meta = this.meta.clone();
340                }
341    
342                if (this.data != null) {
343                    itemStack.data = this.data.clone();
344                }
345    
346                return itemStack;
347            } catch (CloneNotSupportedException e) {
348                throw new Error(e);
349            }
350        }
351    
352        @Override
353        @Utility
354        public final int hashCode() {
355            int hash = 1;
356    
357            hash = hash * 31 + getTypeId();
358            hash = hash * 31 + getAmount();
359            hash = hash * 31 + (getDurability() & 0xffff);
360            hash = hash * 31 + (hasItemMeta() ? (meta == null ? getItemMeta().hashCode() : meta.hashCode()) : 0);
361    
362            return hash;
363        }
364    
365        /**
366         * Checks if this ItemStack contains the given {@link Enchantment}
367         *
368         * @param ench Enchantment to test
369         * @return True if this has the given enchantment
370         */
371        public boolean containsEnchantment(Enchantment ench) {
372            return meta == null ? false : meta.hasEnchant(ench);
373        }
374    
375        /**
376         * Gets the level of the specified enchantment on this item stack
377         *
378         * @param ench Enchantment to check
379         * @return Level of the enchantment, or 0
380         */
381        public int getEnchantmentLevel(Enchantment ench) {
382            return meta == null ? 0 : meta.getEnchantLevel(ench);
383        }
384    
385        /**
386         * Gets a map containing all enchantments and their levels on this item.
387         *
388         * @return Map of enchantments.
389         */
390        public Map<Enchantment, Integer> getEnchantments() {
391            return meta == null ? ImmutableMap.<Enchantment, Integer>of() : meta.getEnchants();
392        }
393    
394        /**
395         * Adds the specified enchantments to this item stack.
396         * <p>
397         * This method is the same as calling {@link
398         * #addEnchantment(org.bukkit.enchantments.Enchantment, int)} for each
399         * element of the map.
400         *
401         * @param enchantments Enchantments to add
402         * @throws IllegalArgumentException if the specified enchantments is null
403         * @throws IllegalArgumentException if any specific enchantment or level
404         *     is null. <b>Warning</b>: Some enchantments may be added before this
405         *     exception is thrown.
406         */
407        @Utility
408        public void addEnchantments(Map<Enchantment, Integer> enchantments) {
409            Validate.notNull(enchantments, "Enchantments cannot be null");
410            for (Map.Entry<Enchantment, Integer> entry : enchantments.entrySet()) {
411                addEnchantment(entry.getKey(), entry.getValue());
412            }
413        }
414    
415        /**
416         * Adds the specified {@link Enchantment} to this item stack.
417         * <p>
418         * If this item stack already contained the given enchantment (at any
419         * level), it will be replaced.
420         *
421         * @param ench Enchantment to add
422         * @param level Level of the enchantment
423         * @throws IllegalArgumentException if enchantment null, or enchantment is
424         *     not applicable
425         */
426        @Utility
427        public void addEnchantment(Enchantment ench, int level) {
428            Validate.notNull(ench, "Enchantment cannot be null");
429            if ((level < ench.getStartLevel()) || (level > ench.getMaxLevel())) {
430                throw new IllegalArgumentException("Enchantment level is either too low or too high (given " + level + ", bounds are " + ench.getStartLevel() + " to " + ench.getMaxLevel() + ")");
431            } else if (!ench.canEnchantItem(this)) {
432                throw new IllegalArgumentException("Specified enchantment cannot be applied to this itemstack");
433            }
434    
435            addUnsafeEnchantment(ench, level);
436        }
437    
438        /**
439         * Adds the specified enchantments to this item stack in an unsafe manner.
440         * <p>
441         * This method is the same as calling {@link
442         * #addUnsafeEnchantment(org.bukkit.enchantments.Enchantment, int)} for
443         * each element of the map.
444         *
445         * @param enchantments Enchantments to add
446         */
447        @Utility
448        public void addUnsafeEnchantments(Map<Enchantment, Integer> enchantments) {
449            for (Map.Entry<Enchantment, Integer> entry : enchantments.entrySet()) {
450                addUnsafeEnchantment(entry.getKey(), entry.getValue());
451            }
452        }
453    
454        /**
455         * Adds the specified {@link Enchantment} to this item stack.
456         * <p>
457         * If this item stack already contained the given enchantment (at any
458         * level), it will be replaced.
459         * <p>
460         * This method is unsafe and will ignore level restrictions or item type.
461         * Use at your own discretion.
462         *
463         * @param ench Enchantment to add
464         * @param level Level of the enchantment
465         */
466        public void addUnsafeEnchantment(Enchantment ench, int level) {
467            (meta == null ? meta = Bukkit.getItemFactory().getItemMeta(getType0()) : meta).addEnchant(ench, level, true);
468        }
469    
470        /**
471         * Removes the specified {@link Enchantment} if it exists on this
472         * ItemStack
473         *
474         * @param ench Enchantment to remove
475         * @return Previous level, or 0
476         */
477        public int removeEnchantment(Enchantment ench) {
478            int level = getEnchantmentLevel(ench);
479            if (level == 0 || meta == null) {
480                return level;
481            }
482            meta.removeEnchant(ench);
483            return level;
484        }
485    
486        @Utility
487        public Map<String, Object> serialize() {
488            Map<String, Object> result = new LinkedHashMap<String, Object>();
489    
490            result.put("type", getType().name());
491    
492            if (getDurability() != 0) {
493                result.put("damage", getDurability());
494            }
495    
496            if (getAmount() != 1) {
497                result.put("amount", getAmount());
498            }
499    
500            ItemMeta meta = getItemMeta();
501            if (!Bukkit.getItemFactory().equals(meta, null)) {
502                result.put("meta", meta);
503            }
504    
505            return result;
506        }
507    
508        /**
509         * Required method for configuration serialization
510         *
511         * @param args map to deserialize
512         * @return deserialized item stack
513         * @see ConfigurationSerializable
514         */
515        public static ItemStack deserialize(Map<String, Object> args) {
516            Material type = Material.getMaterial((String) args.get("type"));
517            short damage = 0;
518            int amount = 1;
519    
520            if (args.containsKey("damage")) {
521                damage = ((Number) args.get("damage")).shortValue();
522            }
523    
524            if (args.containsKey("amount")) {
525                amount = (Integer) args.get("amount");
526            }
527    
528            ItemStack result = new ItemStack(type, amount, damage);
529    
530            if (args.containsKey("enchantments")) { // Backward compatiblity, @deprecated
531                Object raw = args.get("enchantments");
532    
533                if (raw instanceof Map) {
534                    Map<?, ?> map = (Map<?, ?>) raw;
535    
536                    for (Map.Entry<?, ?> entry : map.entrySet()) {
537                        Enchantment enchantment = Enchantment.getByName(entry.getKey().toString());
538    
539                        if ((enchantment != null) && (entry.getValue() instanceof Integer)) {
540                            result.addUnsafeEnchantment(enchantment, (Integer) entry.getValue());
541                        }
542                    }
543                }
544            } else if (args.containsKey("meta")) { // We cannot and will not have meta when enchantments (pre-ItemMeta) exist
545                Object raw = args.get("meta");
546                if (raw instanceof ItemMeta) {
547                    result.setItemMeta((ItemMeta) raw);
548                }
549            }
550    
551            return result;
552        }
553    
554        /**
555         * Get a copy of this ItemStack's {@link ItemMeta}.
556         *
557         * @return a copy of the current ItemStack's ItemData
558         */
559        public ItemMeta getItemMeta() {
560            return this.meta == null ? Bukkit.getItemFactory().getItemMeta(getType0()) : this.meta.clone();
561        }
562    
563        /**
564         * Checks to see if any meta data has been defined.
565         *
566         * @return Returns true if some meta data has been set for this item
567         */
568        public boolean hasItemMeta() {
569            return !Bukkit.getItemFactory().equals(meta, null);
570        }
571    
572        /**
573         * Set the ItemMeta of this ItemStack.
574         *
575         * @param itemMeta new ItemMeta, or null to indicate meta data be cleared.
576         * @return True if successfully applied ItemMeta, see {@link
577         *     ItemFactory#isApplicable(ItemMeta, ItemStack)}
578         * @throws IllegalArgumentException if the item meta was not created by
579         *     the {@link ItemFactory}
580         */
581        public boolean setItemMeta(ItemMeta itemMeta) {
582            return setItemMeta0(itemMeta, getType0());
583        }
584    
585        /*
586         * Cannot be overridden, so it's safe for constructor call
587         */
588        private boolean setItemMeta0(ItemMeta itemMeta, Material material) {
589            if (itemMeta == null) {
590                this.meta = null;
591                return true;
592            }
593            if (!Bukkit.getItemFactory().isApplicable(itemMeta, material)) {
594                return false;
595            }
596            this.meta = Bukkit.getItemFactory().asMetaFor(itemMeta, material);
597            if (this.meta == itemMeta) {
598                this.meta = itemMeta.clone();
599            }
600    
601            return true;
602        }
603    }