/**
* @author Luke Tyler Downey
* Copyright 2011 Glow Interactive
*
* This software contains original work and/or modifications to
* original work, which are redistributed under the following terms.
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Copyright 2011 Brian Cairns
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.glowinteractive.reforger;
import java.net.URL;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringEscapeUtils;
import org.htmlcleaner.CleanerTransformations;
import org.htmlcleaner.CommentNode;
import org.htmlcleaner.ContentNode;
import org.htmlcleaner.HtmlNode;
import org.htmlcleaner.HtmlCleaner;
import org.htmlcleaner.TagNode;
import org.htmlcleaner.TagNodeVisitor;
import org.htmlcleaner.TagTransformation;
public final class Item implements Comparable<Item>, TagNodeVisitor {
private static final String TOOLTIP_FORMAT = "tooltip_enus: '(.*)'";
private static final String TOOLTIP_RATING_NON_RANDOM = "Equip\\: Increases your ([ \\w]+) rating by (\\d+)";
private static final String TOOLTIP_RATING_RANDOM_OR_BONUS = "\\+?(\\d+) ([ \\w&&[^\\d]]+) [rR]ating";
private boolean _parsed = false;
private int _slot;
private String _name;
private TagNode _data;
private final UUID _uniqueID;
private StatKVMap _mutableStats;
private StatKVMap _immutableStats;
private StatKVMap _currentReforging;
private Item() {
_uniqueID = UUID.randomUUID();
_mutableStats = new StatKVMap();
_immutableStats = new StatKVMap();
_currentReforging = new StatKVMap();
}
public Item(int slot, TagNode data) {
this();
_slot = slot;
_data = data;
}
@Override public int compareTo(Item o) {
return Integer.valueOf(_slot).compareTo(o._slot);
}
@Override public boolean equals(Object other) {
if (other instanceof Item) {
Item o = (Item) other;
return _uniqueID.equals(o._uniqueID);
}
return false;
}
@Override public int hashCode() {
return _uniqueID.hashCode();
}
@Override public String toString() {
parse();
// StatKVMap stats = _mutableStats.add(_immutableStats);
// Previously: "[\"" + _name + "\" - Stats: " + stats + "]"
return _name;
}
public synchronized void parse() {
if (!_parsed) {
String[] pair;
String[] elements;
String attribute;
URL url = null;
StringBuilder wowhead = new StringBuilder("http://www.wowhead.com/");
TagNode ref = null;
//<editor-fold defaultstate="collapsed" desc="Parse name.">
ref = _data.findElementByAttValue("class", "name-shadow", true, true);
assert ref != null && ref.getText() != null : "Error: unable to determine item name.";
_name = (ref != null) ? StringEscapeUtils.unescapeHtml4(ref.getText().toString()) : "";
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Parse item ID.">
ref = _data.findElementByName("a", false);
assert ref != null : "Error: unable to determine item attributes.";
attribute = ref.getAttributeByName("href");
elements = attribute.split("/wow/en/item/");
assert elements.length == 2 : "Error: unexpected Armory data format.";
wowhead.append("item=").append(elements[1]);
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Extract data-item string.">
ref = _data.findElementByName("a", false);
assert ref != null : "Error: unable to determine item attributes.";
attribute = ref.getAttributeByName("data-item");
elements = StringEscapeUtils.unescapeHtml4((attribute != null) ? attribute : "").split("&");
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Parse Armory data-item attributes.">
for (String e : elements) {
pair = e.split("=");
if ("e".equals(pair[0])) {
// Permanent Enchantment
wowhead.append("&ench=").append(pair[1]);
}
if ("re".equals(pair[0])) {
// Reforge ID (not currently supported by Wowhead)
wowhead.append("&rf=").append(pair[1]);
}
if ("es".equals(pair[0])) {
// Additional Socket
wowhead.append("&sock");
}
if ("r".equals(pair[0])) {
// Random Itemization
wowhead.append("&rand=").append(pair[1]);
}
if ("set".equals(pair[0])) {
// Set Pieces Equipped
wowhead.append("&pcs=").append(pair[1].replace(',', ':'));
}
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Parse Armory gem ID's.">
TagNode[] gems = _data.getElementsByAttValue("class", "gem", true, true);
final int GEM_COUNT = gems.length;
if (GEM_COUNT != 0) {
String suffix;
wowhead.append("&gems=");
for (int i = 0; i < GEM_COUNT; ++i) {
suffix = gems[i].getAttributeByName("href").replace("/wow/en/item/", "");
wowhead.append(suffix);
if (i + 1 < GEM_COUNT) {
wowhead.append(":");
}
}
}
//</editor-fold>
wowhead.append("&power");
System.out.println(" " + _name);
//<editor-fold defaultstate="collapsed" desc="Download and parse Wowhead JSON data.">
try {
url = new URL(wowhead.toString());
} catch (Exception e) {
Logger.getLogger(Item.class.getSimpleName()).log(Level.SEVERE, null, e);
}
Pattern p = Pattern.compile(TOOLTIP_FORMAT);
Matcher m = p.matcher(URLRetriever.fetchContents(url));
String itemPayload = (m.find()) ? m.group(1) : "";
HtmlCleaner parser = new HtmlCleaner();
CleanerTransformations transform = new CleanerTransformations();
TagTransformation strip = new TagTransformation("small");
transform.addTransformation(strip);
parser.setTransformations(transform);
TagNode root = parser.clean(itemPayload);
//</editor-fold>
root.traverse(this);
System.out.println();
_parsed = true;
}
}
@Override public boolean visit(TagNode parent, HtmlNode current) {
Pattern p = null;
Matcher m = null;
String text = null;
Stat key = null;
int value = -1;
boolean PARSE_MUTABLE = false;
int GROUP_INDEX_KEY = -1,
GROUP_INDEX_VAL = -1;
if (current instanceof CommentNode) {
final CommentNode c = (CommentNode) current;
final String content = c.getCommentedContent();
if (content.matches("<!--rtg\\d\\d-->")) {
//<editor-fold defaultstate="collapsed" desc="Secondary Stats for Non-Random Itemization Pieces.">
// e.g.: "Equip: Increases your critical strike rating by 168"
GROUP_INDEX_KEY = 1;
GROUP_INDEX_VAL = 2;
PARSE_MUTABLE = true;
text = parent.getText().toString().replace(" ", " ");
p = Pattern.compile(TOOLTIP_RATING_NON_RANDOM);
//</editor-fold>
}
if (content.matches("<!--ee-->")) {
//<editor-fold defaultstate="collapsed" desc="Permanent enchant effects.">
// e.g.: "+190 Attack Power and +55 Critical Strike Rating"
GROUP_INDEX_KEY = 2;
GROUP_INDEX_VAL = 1;
PARSE_MUTABLE = false;
text = parent.getText().toString();
p = Pattern.compile(TOOLTIP_RATING_RANDOM_OR_BONUS);
//</editor-fold>
}
}
if (current instanceof ContentNode) {
final ContentNode c = (ContentNode) current;
final String content = c.getContent().toString();
if ("q1".equals(parent.getAttributeByName("class"))) {
//<editor-fold defaultstate="collapsed" desc="Secondary Stats for Random Itemization Pieces.">
// e.g.: "+168 Critical Strike Rating"
GROUP_INDEX_KEY = 2;
GROUP_INDEX_VAL = 1;
PARSE_MUTABLE = true;
text = content;
p = Pattern.compile(TOOLTIP_RATING_RANDOM_OR_BONUS);
//</editor-fold>
}
if (parent.getAttributeByName("class") != null && parent.getAttributeByName("class").startsWith("socket-")) {
//<editor-fold defaultstate="collapsed" desc="Secondary Stats for gems.">
// e.g.: "+20 Agility and +20 Mastery Rating"
GROUP_INDEX_KEY = 2;
GROUP_INDEX_VAL = 1;
PARSE_MUTABLE = false;
text = content;
p = Pattern.compile(TOOLTIP_RATING_RANDOM_OR_BONUS);
//</editor-fold>
}
if (content.startsWith("Socket Bonus") && "q2".equals(parent.getAttributeByName("class"))) {
//<editor-fold defaultstate="collapsed" desc="Secondary Stats for Socket Bonuses.">
// e.g.: "Socket Bonus: +30 Haste"
GROUP_INDEX_KEY = 2;
GROUP_INDEX_VAL = 1;
PARSE_MUTABLE = false;
text = content;
p = Pattern.compile(TOOLTIP_RATING_RANDOM_OR_BONUS);
//</editor-fold>
}
}
if (text == null) {
// Didn't match any known constructs.
return true;
}
m = p.matcher(StringEscapeUtils.unescapeHtml4(text));
while (m.find()) {
key = null;
value = Integer.parseInt(m.group(GROUP_INDEX_VAL));
for (Stat s : Stat.values()) {
if (s.shortName().equalsIgnoreCase(m.group(GROUP_INDEX_KEY))) {
key = s;
}
}
// TODO: Refactor output to parse() via mutable / immutable extension to StatKVMap.
if (key != null) {
if (PARSE_MUTABLE) {
_mutableStats = _mutableStats.add(new StatKVPair(key, value));
System.out.println(String.format(" %+5d", value) + " " + key.shortName());
} else {
_immutableStats = _immutableStats.add(new StatKVPair(key, value));
System.out.println(String.format(" %+5d", value) + " " + key.shortName() + " [Immutable]");
}
}
}
return true;
}
public StatKVMap mutableStats() {
parse();
return _mutableStats;
}
public StatKVMap immutableStats() {
parse();
return _immutableStats;
}
public StatKVMap currentReforging() {
parse();
return _currentReforging;
}
public String name() {
parse();
return _name;
}
public int slot() {
return _slot;
}
public HashSet<StatKVMap> candidates(EnumMap<Stat, EnumSet<Stat>> mappings) {
parse();
final HashSet<StatKVMap> result = new HashSet<StatKVMap>(Stat.TYPE_COUNT);
for (Stat decrease : Stat.values()) {
for (Stat increase : Stat.values()) {
if (_mutableStats.value(increase) == 0 && _mutableStats.value(decrease) != 0
&& mappings.containsKey(decrease) && mappings.get(decrease).contains(increase)) {
int delta = Math.round((float) Math.floor(0.4 * _mutableStats.value(decrease)));
StatKVMap deltaMap = new StatKVMap(decrease, increase, delta);
result.add(deltaMap);
}
}
}
return result;
}
}