wurfl 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +167 -153
- data/VERSION +1 -1
- data/lib/wurfl/command/comparator.rb +15 -32
- data/lib/wurfl/command/uamatch.rb +82 -0
- data/lib/wurfl/handset.rb +3 -18
- data/lib/wurfl/user_agent_matcher.rb +142 -0
- data/test/handset_test.rb +26 -9
- data/test/user_agent_matcher_test.rb +34 -0
- data/wurfl.gemspec +7 -4
- metadata +6 -3
- data/lib/wurfl/command/uaprofwurflcomparator.rb +0 -224
data/README.rdoc
CHANGED
@@ -1,153 +1,167 @@
|
|
1
|
-
= WURFL
|
2
|
-
|
3
|
-
WURFL is a collection of libraries and command line tools for using and manipulating the WURFL. To start using it, simply install the gem:
|
4
|
-
|
5
|
-
sudo gem install wurfl
|
6
|
-
|
7
|
-
== Tools
|
8
|
-
|
9
|
-
All tools are run via wurfltools.rb
|
10
|
-
|
11
|
-
=== loader
|
12
|
-
|
13
|
-
Is used to parse and load a WURFL XML file into memory or save a
|
14
|
-
PStore database that is used by most of the other tools. This
|
15
|
-
application creates WURFL PStore databases that are essential for use
|
16
|
-
with the other Ruby applications.
|
17
|
-
|
18
|
-
=== inspector
|
19
|
-
|
20
|
-
Is a tool that will let you do various searches and queries on the
|
21
|
-
WURFL. This is a very simple, yet powerful program for finding
|
22
|
-
information about the handsets in the WURFL. See the wurflinspector
|
23
|
-
examples section for usage.
|
24
|
-
|
25
|
-
==== Usage
|
26
|
-
|
27
|
-
The command below will search through all handsets and return the ids
|
28
|
-
of handsets that match the passed Ruby boolean evaluation
|
29
|
-
|
30
|
-
This command will return all handsets that have more than 2 colors.
|
31
|
-
|
32
|
-
wurfltools.rb inspector -d pstorehandsets.db -s '{ |hand| hand["colors"].to_i > 2 }'"
|
33
|
-
|
34
|
-
The Ruby query must go in between the single quote marks and needs to
|
35
|
-
declare the WurflHandset instance variable name.
|
36
|
-
|
37
|
-
This command shows you how you can cheat with the current design of
|
38
|
-
the wurflinspector and print more user friendly results. This example
|
39
|
-
assumes you have the command line programs sort and uniq, but that is
|
40
|
-
only to make the output look better. This example does the same as the
|
41
|
-
above, except that it prints out the brand name and model name of the
|
42
|
-
matching handsets instead of the WURFL id.
|
43
|
-
|
44
|
-
Note: this should all go into one command line call
|
45
|
-
|
46
|
-
wurfltools.rb inspector -d pstorehandsets.db -s '{|hand| puts
|
47
|
-
"#{hand["brand_name"]} #{hand["model_name"]}" if hand["colors"].to_i >
|
48
|
-
2}' | sort | uniq"
|
49
|
-
|
50
|
-
|
51
|
-
The following individual handset query commands will tell the value of
|
52
|
-
capabilities and from where it obtained the setting.
|
53
|
-
|
54
|
-
A command to query the handset with the id sonyericsson_t300_ver1
|
55
|
-
for all of its' capabilities:
|
56
|
-
|
57
|
-
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1
|
58
|
-
|
59
|
-
A command to query the handset with the id sonyericsson_t300_ver1 for
|
60
|
-
backlight capability:
|
61
|
-
|
62
|
-
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1 -q
|
63
|
-
backlight
|
64
|
-
|
65
|
-
===
|
66
|
-
|
67
|
-
Is a
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
The
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
the
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
1
|
+
= WURFL
|
2
|
+
|
3
|
+
WURFL is a collection of libraries and command line tools for using and manipulating the WURFL. To start using it, simply install the gem:
|
4
|
+
|
5
|
+
sudo gem install wurfl
|
6
|
+
|
7
|
+
== Tools
|
8
|
+
|
9
|
+
All tools are run via wurfltools.rb
|
10
|
+
|
11
|
+
=== loader
|
12
|
+
|
13
|
+
Is used to parse and load a WURFL XML file into memory or save a
|
14
|
+
PStore database that is used by most of the other tools. This
|
15
|
+
application creates WURFL PStore databases that are essential for use
|
16
|
+
with the other Ruby applications.
|
17
|
+
|
18
|
+
=== inspector
|
19
|
+
|
20
|
+
Is a tool that will let you do various searches and queries on the
|
21
|
+
WURFL. This is a very simple, yet powerful program for finding
|
22
|
+
information about the handsets in the WURFL. See the wurflinspector
|
23
|
+
examples section for usage.
|
24
|
+
|
25
|
+
==== Usage
|
26
|
+
|
27
|
+
The command below will search through all handsets and return the ids
|
28
|
+
of handsets that match the passed Ruby boolean evaluation
|
29
|
+
|
30
|
+
This command will return all handsets that have more than 2 colors.
|
31
|
+
|
32
|
+
wurfltools.rb inspector -d pstorehandsets.db -s '{ |hand| hand["colors"].to_i > 2 }'"
|
33
|
+
|
34
|
+
The Ruby query must go in between the single quote marks and needs to
|
35
|
+
declare the WurflHandset instance variable name.
|
36
|
+
|
37
|
+
This command shows you how you can cheat with the current design of
|
38
|
+
the wurflinspector and print more user friendly results. This example
|
39
|
+
assumes you have the command line programs sort and uniq, but that is
|
40
|
+
only to make the output look better. This example does the same as the
|
41
|
+
above, except that it prints out the brand name and model name of the
|
42
|
+
matching handsets instead of the WURFL id.
|
43
|
+
|
44
|
+
Note: this should all go into one command line call
|
45
|
+
|
46
|
+
wurfltools.rb inspector -d pstorehandsets.db -s '{|hand| puts
|
47
|
+
"#{hand["brand_name"]} #{hand["model_name"]}" if hand["colors"].to_i >
|
48
|
+
2}' | sort | uniq"
|
49
|
+
|
50
|
+
|
51
|
+
The following individual handset query commands will tell the value of
|
52
|
+
capabilities and from where it obtained the setting.
|
53
|
+
|
54
|
+
A command to query the handset with the id sonyericsson_t300_ver1
|
55
|
+
for all of its' capabilities:
|
56
|
+
|
57
|
+
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1
|
58
|
+
|
59
|
+
A command to query the handset with the id sonyericsson_t300_ver1 for
|
60
|
+
backlight capability:
|
61
|
+
|
62
|
+
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1 -q
|
63
|
+
backlight
|
64
|
+
|
65
|
+
=== uamatch
|
66
|
+
|
67
|
+
Is a tool that will let you do various searches and queries on the
|
68
|
+
WURFL based on user-agent strings. This is a very simple, yet powerful
|
69
|
+
program for finding information about the handsets in the WURFL.
|
70
|
+
See the uamatch examples section for usage.
|
71
|
+
|
72
|
+
The uamatcher will retrieve the handset descriptions with the closest
|
73
|
+
Levenshtein distance (also called the edit distance) to the user-agent
|
74
|
+
string you pass as argument.
|
75
|
+
|
76
|
+
For details see: http://en.wikipedia.org/wiki/Levenshtein_distance
|
77
|
+
|
78
|
+
When more than one handset has the same distance to the user agent string queried,
|
79
|
+
the command will iterate over the result set and print the result for each handset.
|
80
|
+
|
81
|
+
The found wurfl_ids can serve as input to the inspector command.
|
82
|
+
You can also specifically query for a certain capability.
|
83
|
+
|
84
|
+
==== Usage
|
85
|
+
|
86
|
+
The following individual handset query commands will tell the value of
|
87
|
+
capabilities and from where it obtained the setting.
|
88
|
+
|
89
|
+
A command to query the handset with the user-agent SL55 for all of its capabilities:
|
90
|
+
|
91
|
+
wurfltools.rb uamatch -d pstorehandsets.db -u SL55
|
92
|
+
|
93
|
+
A command to query the handset with the id SL55 for the wallpaper_jpg capability:
|
94
|
+
|
95
|
+
wurfltools.rb uamatch -d pstorehandsets.db -u SL55 -q wallpaper_jpg
|
96
|
+
|
97
|
+
=== sanitycheck
|
98
|
+
|
99
|
+
Is a partial WURFL validating program. It does a few simple
|
100
|
+
checks to make sure the XML structure is parse-able by the wurflloader.
|
101
|
+
If you receive loading errors by the wurflloader, then you can run the
|
102
|
+
wurflsanitycheck program to find the lines in the XML file that might
|
103
|
+
be causing the problem.
|
104
|
+
|
105
|
+
=== comparator
|
106
|
+
|
107
|
+
Is a another simple program that will find the differences from two
|
108
|
+
WURFL Ruby PStore databases. This is another way of finding changes
|
109
|
+
from the different versions of the WURFL without running a diff on the
|
110
|
+
XML files.
|
111
|
+
|
112
|
+
=== uaproftowurfl
|
113
|
+
|
114
|
+
Is a program that takes UAProfiles and creates an equivalent WURFL
|
115
|
+
entry. It holds all of the mappings used to convert a UAProfile to a
|
116
|
+
WURFL entry. This program alone makes using the Ruby tools worth it.
|
117
|
+
|
118
|
+
The mappings are not fully complete and can certainly use your input
|
119
|
+
in improving them.
|
120
|
+
|
121
|
+
About the source code:
|
122
|
+
The main piece of code to read are the methods under the
|
123
|
+
"UAProfile Mappings" comment.
|
124
|
+
|
125
|
+
For now you can ignore the details above this comment.
|
126
|
+
Basically each method is the name of a UAProfifle component.
|
127
|
+
When you parse a UAProfile file it will call each UAProfile
|
128
|
+
component's method name.
|
129
|
+
|
130
|
+
So you can simply look at UAProfile component's method to see
|
131
|
+
how it maps to the WURFL.
|
132
|
+
If a component is not found as a method it will be logged to
|
133
|
+
Standard Error. For this log one can then add the method to
|
134
|
+
the UAProfToWurfl class later.
|
135
|
+
|
136
|
+
==== Usage
|
137
|
+
|
138
|
+
A simple usage is:
|
139
|
+
|
140
|
+
wurfltools.rb uaproftowurfl sampleprofile.xml
|
141
|
+
|
142
|
+
Example use from a bash shell with many profiles:
|
143
|
+
|
144
|
+
for i in `ls profiles`; do
|
145
|
+
wurfltools.rb uaproftowurfl profiles/$i >output/$i.wurfl
|
146
|
+
2> output/$i.parser.err;
|
147
|
+
done
|
148
|
+
|
149
|
+
This assumes that you have a profiles directory with all of the UAProf
|
150
|
+
file you wish to parse and a output directory to place the results and
|
151
|
+
errors.
|
152
|
+
|
153
|
+
== Authors
|
154
|
+
|
155
|
+
* Zev Blut (Original Author)
|
156
|
+
* Paul McMahon (gem installer, refactorings)
|
157
|
+
* Kai W. Zimmermann (uamatch)
|
158
|
+
|
159
|
+
== Copyright
|
160
|
+
|
161
|
+
Copyright (c) 2009, mobalean (http://www.mobalean.com/)
|
162
|
+
|
163
|
+
Copyright (c) 2003, Ubiquitous Business Technology (http://ubit.com)
|
164
|
+
All rights reserved.
|
165
|
+
|
166
|
+
Send enquiries to: mailto:info@mobalean.com
|
167
|
+
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.2.0
|
@@ -13,10 +13,11 @@ class Wurfl::Command::Comparator < Wurfl::Command
|
|
13
13
|
|
14
14
|
def display_differences(hand1, hand2)
|
15
15
|
puts "-------------------------------------"
|
16
|
-
puts "
|
17
|
-
hand1.
|
16
|
+
puts "WURFL_ID: #{hand1.wurfl_id}"
|
17
|
+
puts "Handset 1: #{hand1.user_agent}"
|
18
|
+
puts "Handset 2: #{hand2.user_agent}"
|
19
|
+
hand1.differences(hand2).each do |key|
|
18
20
|
v1, v2 = hand1[key], hand2[key]
|
19
|
-
next if v1.nil? || v2.nil? || v1 == v2
|
20
21
|
puts "Key:#{key}"
|
21
22
|
puts "h1>:#{hand1[key]}"
|
22
23
|
puts "h2<:#{hand2[key]}"
|
@@ -49,40 +50,22 @@ class Wurfl::Command::Comparator < Wurfl::Command
|
|
49
50
|
puts "Comparing files: #{ARGV[0]} and #{ARGV[1]}"
|
50
51
|
puts "-------------------------------------"
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
different = Array.new
|
62
|
-
mwurfl.each do |key,handset|
|
63
|
-
if lwurfl.key?(key)
|
64
|
-
if handset != lwurfl[key]
|
65
|
-
different << [handset,lwurfl[key]]
|
66
|
-
display_differences(handset,lwurfl[key])
|
67
|
-
end
|
68
|
-
else
|
69
|
-
notfound<< handset
|
53
|
+
wurfl1_unknown, wurfl2_unknown, different = [],[],[]
|
54
|
+
(wurfl1.keys | wurfl2.keys).each do |key|
|
55
|
+
handset1, handset2 = wurfl1[key], wurfl2[key]
|
56
|
+
if !handset1
|
57
|
+
wurfl1_unknown << key
|
58
|
+
elsif !handset2
|
59
|
+
wurfl2_unknown << key
|
60
|
+
elsif handset1 != handset2
|
61
|
+
display_differences(handset1,handset2)
|
70
62
|
end
|
71
63
|
end
|
72
64
|
|
73
65
|
|
74
66
|
puts "Comparision complete."
|
75
67
|
|
76
|
-
puts "
|
77
|
-
puts "
|
78
|
-
notfound = notfound.sort { |x,y| y.wurfl_id <=> x.wurfl_id }
|
79
|
-
notfound.each { |hand| puts hand.wurfl_id }
|
80
|
-
puts "||||||||||||||||||||||||||||||||||||"
|
81
|
-
|
82
|
-
puts "Different handsets: #{different.size}"
|
83
|
-
puts "||||||||||||||||||||||||||||||||||||"
|
84
|
-
different = different.sort { |x,y| y.first.wurfl_id <=> x.first.wurfl_id }
|
85
|
-
|
86
|
-
puts "||||||||||||||||||||||||||||||||||||"
|
68
|
+
puts "Handsets not found in wurfl1: #{wurfl1_unknown.inspect}"
|
69
|
+
puts "Handsets not found in wurfl2: #{wurfl2_unknown.inspect}"
|
87
70
|
end
|
88
71
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "wurfl/command"
|
2
|
+
require "wurfl/utils"
|
3
|
+
require "wurfl/handset"
|
4
|
+
require "wurfl/user_agent_matcher"
|
5
|
+
require "getoptlong"
|
6
|
+
|
7
|
+
=begin
|
8
|
+
The uamatch command itself is based on the inspector command found in this
|
9
|
+
library.
|
10
|
+
|
11
|
+
Author: Kai W. Zimmermann (kwz@kai-zimmermann.de)
|
12
|
+
=end
|
13
|
+
class Wurfl::Command::Uamatch < Wurfl::Command
|
14
|
+
include Wurfl::Utils
|
15
|
+
|
16
|
+
def usage
|
17
|
+
puts "Usage: wurfltools.rb uamatch [-u user_agent [-q attributename]] -d pstorefile"
|
18
|
+
puts "Examples:"
|
19
|
+
puts "wurfltools.rb uamatch -d pstorehandsets.db -u SL55"
|
20
|
+
puts "wurfltools.rb uamatch -d pstorehandsets.db -u SL55 -q wallpaper_jpg"
|
21
|
+
exit 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
pstorefile = nil
|
26
|
+
useragent = nil
|
27
|
+
query = nil
|
28
|
+
begin
|
29
|
+
opt = GetoptLong.new(
|
30
|
+
["-d","--database", GetoptLong::REQUIRED_ARGUMENT],
|
31
|
+
["-u","--useragent", GetoptLong::REQUIRED_ARGUMENT],
|
32
|
+
["-q","--query", GetoptLong::REQUIRED_ARGUMENT]
|
33
|
+
)
|
34
|
+
|
35
|
+
opt.each do |arg,val|
|
36
|
+
case arg
|
37
|
+
when "-d"
|
38
|
+
pstorefile = val
|
39
|
+
when "-u"
|
40
|
+
useragent = val
|
41
|
+
when "-q"
|
42
|
+
query = val
|
43
|
+
else
|
44
|
+
usage
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue => err
|
49
|
+
usage
|
50
|
+
end
|
51
|
+
|
52
|
+
if !pstorefile
|
53
|
+
puts "You must specify a Wurfl PStore db"
|
54
|
+
usage
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
handsets, = load_wurfl_pstore(pstorefile)
|
59
|
+
puts "Number of Handsets: #{handsets.length}"
|
60
|
+
uamatch = Wurfl::UserAgentMatcher.new(handsets)
|
61
|
+
rescue => err
|
62
|
+
STDERR.puts "Error with file #{pstorefile}"
|
63
|
+
STDERR.puts err.message
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
if useragent
|
68
|
+
matches, distance = uamatch.match_handsets(useragent)
|
69
|
+
puts "Shortest distance #{distance}, #{matches.length} match#{'es' if (matches.length!=1)}"
|
70
|
+
matches.each do |handset|
|
71
|
+
puts "Handset wurfl id: #{handset.wurfl_id}"
|
72
|
+
puts "User_agent found: #{handset.user_agent}"
|
73
|
+
if query
|
74
|
+
puts "Result of handset query: #{query}"
|
75
|
+
rez = handset.get_value_and_owner(query)
|
76
|
+
puts "#{rez[0]} from #{rez[1]}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
exit 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/wurfl/handset.rb
CHANGED
@@ -86,24 +86,9 @@ class Wurfl::Handset
|
|
86
86
|
other.keys.all? {|key| other[key] == self[key] }
|
87
87
|
end
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
# Returns:
|
93
|
-
# An array of the different values.
|
94
|
-
# Each entry in the Array is an Array of three values.
|
95
|
-
# The first value is the key in which both handsets have different values.
|
96
|
-
# The second is the other handset's value for the key.
|
97
|
-
# The third is the handset id from where the other handset got it's value.
|
98
|
-
def compare(other)
|
99
|
-
differences = Array.new
|
100
|
-
self.keys.each do |key|
|
101
|
-
oval,oid = other.get_value_and_owner(key)
|
102
|
-
if @capabilityhash[key].to_s != oval.to_s
|
103
|
-
differences << [key,oval,oid]
|
104
|
-
end
|
105
|
-
end
|
106
|
-
differences
|
89
|
+
def differences(other)
|
90
|
+
keys = (self.keys | other.keys)
|
91
|
+
keys.find_all {|k| self[k] != other[k]}
|
107
92
|
end
|
108
93
|
|
109
94
|
class NullHandset
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Wurfl; end
|
2
|
+
|
3
|
+
=begin
|
4
|
+
A class that lists wurfl handsets that match user_agents using the shortest
|
5
|
+
Levenshtein distance, also sometimes called the edit distance,
|
6
|
+
see http://en.wikipedia.org/wiki/Levenshtein_distance
|
7
|
+
|
8
|
+
The implementation of the Levenshtein distance used here is based on the
|
9
|
+
algorithm found in the Text gem originally written by Paul Battley
|
10
|
+
(pbattley@gmail.com)
|
11
|
+
|
12
|
+
The implementation given here makes heavy use of optimizations based on
|
13
|
+
the estimation of the lower bound that can be achieved. Depending on the
|
14
|
+
length of the user agent this brought an over all increase by a factor of
|
15
|
+
about 40, although it renders the code slightly unreadable. In general the
|
16
|
+
longer the user agent string and the greater the result distance, the longer
|
17
|
+
the computation takes.
|
18
|
+
|
19
|
+
Author: Kai W. Zimmermann (kwz@kai-zimmermann.de)
|
20
|
+
=end
|
21
|
+
class Wurfl::UserAgentMatcher
|
22
|
+
# Constructor
|
23
|
+
# Parameters:
|
24
|
+
# handsets: A hashtable of wurfl handsets indexed by wurfl_id.
|
25
|
+
def initialize(handsets)
|
26
|
+
@handsets = handsets
|
27
|
+
@longest_user_agent_length = @handsets.values.inject(0) do |m,hand|
|
28
|
+
hand.user_agent.length > m ? hand.user_agent.length : m
|
29
|
+
end
|
30
|
+
@d=(0..@longest_user_agent_length).to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
# A method to retrieve a list of the uamatcher's handsets that match
|
34
|
+
# the passed user_agent closest using the Levenshtein distance.
|
35
|
+
# Parameters:
|
36
|
+
# user_agent: is a user_agent string to be matched
|
37
|
+
# Returns:
|
38
|
+
# An Array of all WurflHandsets that match the user_agent closest with the
|
39
|
+
# same distance
|
40
|
+
# The Levenshtein distance for these matches
|
41
|
+
def match_handsets(user_agent)
|
42
|
+
rez = []
|
43
|
+
shortest_distance = [user_agent.length, @longest_user_agent_length].max
|
44
|
+
s = user_agent.unpack(unpack_rule)
|
45
|
+
|
46
|
+
@handsets.values.each do |hand|
|
47
|
+
distance = levenshtein_distance(user_agent, hand.user_agent, shortest_distance, s)
|
48
|
+
# found a shorter distance match, flush old results
|
49
|
+
rez.clear if shortest_distance > distance
|
50
|
+
|
51
|
+
if shortest_distance >= distance
|
52
|
+
# always add the first handset matched and each that has the same
|
53
|
+
# distance as the shortest distance so far
|
54
|
+
rez << hand
|
55
|
+
shortest_distance = distance
|
56
|
+
end
|
57
|
+
|
58
|
+
break if shortest_distance == 0
|
59
|
+
end
|
60
|
+
|
61
|
+
return rez, shortest_distance
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# A method to estimate and compute the Levenshtein distance (LD) based on the
|
67
|
+
# implementation from the Text gem. The implementation given here applies
|
68
|
+
# known upper and lower bounds as found at:
|
69
|
+
# http://en.wikipedia.org/wiki/Levenshtein_distance
|
70
|
+
# LD is always at least the difference of the sizes of the two strings.
|
71
|
+
# -> We can safely discard the handset if the least distance is longer than
|
72
|
+
# the current shortest distance
|
73
|
+
# LD is zero if and only if the strings are identical.
|
74
|
+
# -> We can optimize the test for equality and stop searching after an exact
|
75
|
+
# match
|
76
|
+
# Parameters:
|
77
|
+
# str1: is the user-agent to look up
|
78
|
+
# str2: is the user-agent to compare against
|
79
|
+
# min: is the minimum distance found so far
|
80
|
+
# s: the unpacked version of the user-agent string we look up
|
81
|
+
# Returns:
|
82
|
+
# It returns the least bound estimation if the least bound is already greater
|
83
|
+
# than the current minimum distance
|
84
|
+
# Otherwise it will compute the Levenshtein distance of str1 and str2
|
85
|
+
# It optimizes the check for equality
|
86
|
+
def levenshtein_distance(str1, str2, min, s)
|
87
|
+
diff = (str1.length - str2.length).abs
|
88
|
+
return 0 if diff == 0 && str1 == str2
|
89
|
+
return diff if diff > min
|
90
|
+
t = str2.unpack(unpack_rule)
|
91
|
+
distance(s, t, min)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Compute the Levenshtein distance or stop if the minimum found so far will
|
95
|
+
# be exceeded.
|
96
|
+
# Optimizations: Avoid GC, where possible reuse array
|
97
|
+
# The distance computation in the outer loop is monotonously decreasing thus
|
98
|
+
# we can safely stop if the estimated possible minimum exceeds the current
|
99
|
+
# minimum
|
100
|
+
# Parameters:
|
101
|
+
# s: is the first string, already unpacked
|
102
|
+
# t: is the second string, already unpacked
|
103
|
+
# min: is the minimum distance found so far
|
104
|
+
# Returns:
|
105
|
+
# The routine returns the computed distance or the minimum found so far if the # distance
|
106
|
+
# computed so far exceeds the minimum
|
107
|
+
def distance(s, t, min)
|
108
|
+
n = s.length
|
109
|
+
m = t.length
|
110
|
+
return m if 0 == n
|
111
|
+
return n if 0 == m
|
112
|
+
|
113
|
+
d = @d # Optimization: Avoid GC by reusing array
|
114
|
+
(0...m).each do |j|
|
115
|
+
d[j] = j
|
116
|
+
end
|
117
|
+
|
118
|
+
x = 0
|
119
|
+
(0...n).each do |i|
|
120
|
+
e = i+1
|
121
|
+
(0...m).each do |j|
|
122
|
+
cost = (s[i] == t[j]) ? 0 : 1
|
123
|
+
x = d[j+1] + 1 # insertion
|
124
|
+
y = e+1
|
125
|
+
x = y if y < x # deletion
|
126
|
+
z = d[j] + cost
|
127
|
+
x = z if z < x # substitution
|
128
|
+
d[j] = e
|
129
|
+
e = x
|
130
|
+
end
|
131
|
+
d[m] = x
|
132
|
+
# estimate the minimum LD that still can be achieved, this will be
|
133
|
+
# increasing monotonously stop once we exceed the current minimum
|
134
|
+
return x - n + i + 1 if x - n + i + 1 > min
|
135
|
+
end
|
136
|
+
x
|
137
|
+
end
|
138
|
+
|
139
|
+
def unpack_rule
|
140
|
+
$KCODE =~ /^U/i ? 'U*' : 'C*'
|
141
|
+
end
|
142
|
+
end
|
data/test/handset_test.rb
CHANGED
@@ -6,6 +6,9 @@ class TestHandset < Test::Unit::TestCase
|
|
6
6
|
def setup
|
7
7
|
@f = Wurfl::Handset.new("f", "f", nil)
|
8
8
|
@h = Wurfl::Handset.new("h", "h", @f)
|
9
|
+
|
10
|
+
@f2 = Wurfl::Handset.new("f2", "f2_ua", nil)
|
11
|
+
@h2 = Wurfl::Handset.new("h2","h2_ua", @f2)
|
9
12
|
end
|
10
13
|
|
11
14
|
def test_f
|
@@ -51,20 +54,34 @@ class TestHandset < Test::Unit::TestCase
|
|
51
54
|
assert @h == h2
|
52
55
|
end
|
53
56
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
57
|
+
def test_differences_handset_with_unmodified_fallback
|
58
|
+
assert @h.differences(@f).empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_differences_handset_with_identical_handset
|
62
|
+
assert @h.differences(@h2).empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_differences_handset_that_has_extra_key
|
58
66
|
@h["k"] = "v"
|
67
|
+
assert_equal ["k"], @h.differences(@h2)
|
68
|
+
end
|
59
69
|
|
60
|
-
|
70
|
+
def test_differences_other_handset_that_has_extra_key
|
71
|
+
@h2["k"] = "v"
|
72
|
+
assert_equal ["k"], @h.differences(@h2)
|
73
|
+
end
|
61
74
|
|
62
|
-
|
63
|
-
|
75
|
+
def test_differences_handset_that_has_differing_key_value
|
76
|
+
@h["k"] = "v"
|
77
|
+
@h2["k"] = "v2"
|
78
|
+
assert_equal ["k"], @h.differences(@h2)
|
79
|
+
end
|
64
80
|
|
81
|
+
def test_differences_handsets_with_differening_fallback_key_value
|
65
82
|
@f["j"] = "1"
|
66
|
-
f2["j"] = "2"
|
67
|
-
assert_equal [
|
83
|
+
@f2["j"] = "2"
|
84
|
+
assert_equal ["j"], @h.differences(@h2)
|
68
85
|
end
|
69
86
|
|
70
87
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'wurfl/user_agent_matcher'
|
3
|
+
require 'wurfl/loader'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class UserAgentMatcherTest < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
loader = Wurfl::Loader.new
|
9
|
+
handsets, fallbacks = loader.load_wurfl(File.join(File.dirname(__FILE__), "data", "wurfl.simple.xml"))
|
10
|
+
@matcher = Wurfl::UserAgentMatcher.new(handsets)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_empty_user_agent
|
14
|
+
a, shortest_distance = @matcher.match_handsets("")
|
15
|
+
assert_equal 1, a.size
|
16
|
+
assert_equal "generic_xhtml", a.first.wurfl_id
|
17
|
+
assert_equal 4, shortest_distance
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_matching_user_agent
|
21
|
+
a, shortest_distance = @matcher.match_handsets("generic")
|
22
|
+
assert_equal 1, a.size
|
23
|
+
assert_equal "generic", a.first.wurfl_id
|
24
|
+
assert_equal 0, shortest_distance
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_iphone
|
28
|
+
s = "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en)"
|
29
|
+
a, shortest_distance = @matcher.match_handsets(s)
|
30
|
+
assert_equal 1, a.size
|
31
|
+
assert_equal "apple_generic", a.first.wurfl_id
|
32
|
+
assert_equal 26, shortest_distance
|
33
|
+
end
|
34
|
+
end
|
data/wurfl.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{wurfl}
|
5
|
-
s.version = "1.
|
5
|
+
s.version = "1.2.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Paul McMahon", "Zev Blut"]
|
9
|
-
s.date = %q{2009-07-
|
9
|
+
s.date = %q{2009-07-22}
|
10
10
|
s.default_executable = %q{wurfltools.rb}
|
11
11
|
s.description = %q{TODO}
|
12
12
|
s.email = %q{info@mobalean.com}
|
@@ -27,15 +27,17 @@ Gem::Specification.new do |s|
|
|
27
27
|
"lib/wurfl/command/inspector.rb",
|
28
28
|
"lib/wurfl/command/loader.rb",
|
29
29
|
"lib/wurfl/command/sanitycheck.rb",
|
30
|
+
"lib/wurfl/command/uamatch.rb",
|
30
31
|
"lib/wurfl/command/uaproftowurfl.rb",
|
31
|
-
"lib/wurfl/command/uaprofwurflcomparator.rb",
|
32
32
|
"lib/wurfl/handset.rb",
|
33
33
|
"lib/wurfl/loader.rb",
|
34
34
|
"lib/wurfl/uaproftowurfl.rb",
|
35
|
+
"lib/wurfl/user_agent_matcher.rb",
|
35
36
|
"lib/wurfl/utils.rb",
|
36
37
|
"test/data/wurfl.simple.xml",
|
37
38
|
"test/handset_test.rb",
|
38
39
|
"test/loader_test.rb",
|
40
|
+
"test/user_agent_matcher_test.rb",
|
39
41
|
"test/utils_test.rb",
|
40
42
|
"wurfl.gemspec"
|
41
43
|
]
|
@@ -49,7 +51,8 @@ Gem::Specification.new do |s|
|
|
49
51
|
s.test_files = [
|
50
52
|
"test/loader_test.rb",
|
51
53
|
"test/handset_test.rb",
|
52
|
-
"test/utils_test.rb"
|
54
|
+
"test/utils_test.rb",
|
55
|
+
"test/user_agent_matcher_test.rb"
|
53
56
|
]
|
54
57
|
|
55
58
|
if s.respond_to? :specification_version then
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wurfl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul McMahon
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-07-
|
13
|
+
date: 2009-07-22 00:00:00 +09:00
|
14
14
|
default_executable: wurfltools.rb
|
15
15
|
dependencies: []
|
16
16
|
|
@@ -35,15 +35,17 @@ files:
|
|
35
35
|
- lib/wurfl/command/inspector.rb
|
36
36
|
- lib/wurfl/command/loader.rb
|
37
37
|
- lib/wurfl/command/sanitycheck.rb
|
38
|
+
- lib/wurfl/command/uamatch.rb
|
38
39
|
- lib/wurfl/command/uaproftowurfl.rb
|
39
|
-
- lib/wurfl/command/uaprofwurflcomparator.rb
|
40
40
|
- lib/wurfl/handset.rb
|
41
41
|
- lib/wurfl/loader.rb
|
42
42
|
- lib/wurfl/uaproftowurfl.rb
|
43
|
+
- lib/wurfl/user_agent_matcher.rb
|
43
44
|
- lib/wurfl/utils.rb
|
44
45
|
- test/data/wurfl.simple.xml
|
45
46
|
- test/handset_test.rb
|
46
47
|
- test/loader_test.rb
|
48
|
+
- test/user_agent_matcher_test.rb
|
47
49
|
- test/utils_test.rb
|
48
50
|
- wurfl.gemspec
|
49
51
|
has_rdoc: true
|
@@ -76,3 +78,4 @@ test_files:
|
|
76
78
|
- test/loader_test.rb
|
77
79
|
- test/handset_test.rb
|
78
80
|
- test/utils_test.rb
|
81
|
+
- test/user_agent_matcher_test.rb
|
@@ -1,224 +0,0 @@
|
|
1
|
-
require "wurfl/command"
|
2
|
-
require "getoptlong"
|
3
|
-
require "net/http"
|
4
|
-
|
5
|
-
require "wurfl/uaproftowurfl"
|
6
|
-
require "wurfl/handset"
|
7
|
-
require "wurfl/utils"
|
8
|
-
include Wurfl::Utils
|
9
|
-
|
10
|
-
# An addition to the UAProf to Wurfl to generate a WurflHandset from the UAProf.
|
11
|
-
class Wurfl::UAProfToWURLF
|
12
|
-
def make_wurfl_handset
|
13
|
-
hand = Wurfl::Handset.new("UAProf",@wurfl["user_agent"])
|
14
|
-
@wurflgroups.each do |group|
|
15
|
-
@wurfl[group].sort.each do |key,value|
|
16
|
-
hand[key] = value
|
17
|
-
end
|
18
|
-
end
|
19
|
-
return hand
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
|
-
class Wurfl::Command::Uaprofwurflcomparator
|
25
|
-
def parse_mapping_file(file)
|
26
|
-
if !File.exist?(file)
|
27
|
-
$stderr.puts "Mapping File does not exist. File passed was #{file}."
|
28
|
-
return Array.new
|
29
|
-
end
|
30
|
-
mappings = Array.new
|
31
|
-
f = File.new(file)
|
32
|
-
f.each do |line|
|
33
|
-
if m = /^"(.*)" "(.*)"$/.match(line.strip)
|
34
|
-
uaprof = m[1]
|
35
|
-
useragent = m[2]
|
36
|
-
mappings<< [uaprof,useragent]
|
37
|
-
else
|
38
|
-
$stderr.puts "Irregular format for line: #{line}" if line.strip != ""
|
39
|
-
end
|
40
|
-
end
|
41
|
-
f.close
|
42
|
-
|
43
|
-
return mappings
|
44
|
-
end
|
45
|
-
|
46
|
-
def get_uaprofile(uaprof,profiledir,check=false)
|
47
|
-
file = strip_uaprof(uaprof)
|
48
|
-
if File.exists?("#{profiledir}/#{file}") && check
|
49
|
-
return file
|
50
|
-
end
|
51
|
-
|
52
|
-
get_and_save_uaprof_file(uaprof,profiledir)
|
53
|
-
return file
|
54
|
-
end
|
55
|
-
|
56
|
-
def strip_uaprof(uaprof)
|
57
|
-
uaprof_file = nil
|
58
|
-
if m = /([^\/]*)$/.match(uaprof)
|
59
|
-
uaprof_file = m[1]
|
60
|
-
else
|
61
|
-
$stderr.puts "Cannot find the base UAProf file in URI: #{uaprof}"
|
62
|
-
end
|
63
|
-
return uaprof_file
|
64
|
-
end
|
65
|
-
|
66
|
-
def load_pstore(pstorefile)
|
67
|
-
hands = Hash.new
|
68
|
-
begin
|
69
|
-
handsid, = load_wurfl_pstore(pstorefile)
|
70
|
-
handsid.each { |id,val| hands[val.user_agent] = val }
|
71
|
-
rescue => err
|
72
|
-
$stderr.puts "Error: Cannot load PStore file. #{pstorefile}"
|
73
|
-
$stderr.puts err.message
|
74
|
-
exit 1
|
75
|
-
end
|
76
|
-
return hands
|
77
|
-
end
|
78
|
-
|
79
|
-
def get_and_save_uaprof_file(uaprof_url,savedirectory,limit=0)
|
80
|
-
base,path,port = parse_url(uaprof_url)
|
81
|
-
|
82
|
-
raise "Too many redirects from original url" if limit > 3
|
83
|
-
raise "Unparseable URL: #{url}" if base.nil?
|
84
|
-
|
85
|
-
port = 80 if port.nil?
|
86
|
-
http = Net::HTTP.new(base,port)
|
87
|
-
begin
|
88
|
-
resp, data = http.get(path)
|
89
|
-
if resp.code == "301"
|
90
|
-
# get location and call self again
|
91
|
-
http.finish
|
92
|
-
limit += 1
|
93
|
-
get_and_save_uaprof_file(resp['location'],savedirectory,limit)
|
94
|
-
return
|
95
|
-
elsif resp.code != "200"
|
96
|
-
raise "Unexpected HTTP Response code:#{resp.code} for #{uaprof_url}"
|
97
|
-
end
|
98
|
-
rescue => err
|
99
|
-
raise
|
100
|
-
end
|
101
|
-
|
102
|
-
f = File.new("#{savedirectory}/#{strip_uaprof(path)}","w")
|
103
|
-
f.write(data)
|
104
|
-
f.close
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
def parse_url(url)
|
109
|
-
m = /(http:\/\/)?(.*?)(:(\d*))?\/(.*)/i.match(url.strip)
|
110
|
-
|
111
|
-
return nil if m.nil?
|
112
|
-
return m[2],"/#{m[5]}",m[4]
|
113
|
-
end
|
114
|
-
|
115
|
-
def usage
|
116
|
-
puts "Usage: wurfltools.rb uaprofwurflcomparator -d profiledirectory -f mappingfile [-w wurfldb] [-c] [-h | --help]"
|
117
|
-
puts "Examples:"
|
118
|
-
puts "wurfltools.rb uaprofwurflcomparator -d ./profiles -f all-profile.2003-08.log -c -w wurfl.db"
|
119
|
-
exit 1
|
120
|
-
end
|
121
|
-
|
122
|
-
def help
|
123
|
-
puts "-d --directory : The directory to store the UA Profiles found in the log file."
|
124
|
-
puts "-f --file : The log file that has a UAProfile to User-Agent mapping per line."
|
125
|
-
puts "-c --check : A flag that will make sure to check if the profile is already in the directory or not. If it is not then it will download it."
|
126
|
-
puts "-w --wurfldb : A Ruby PStore Database of the WURFL, that is used to compare against the UAProfiles."
|
127
|
-
puts "-h --help : This message."
|
128
|
-
exit 1
|
129
|
-
end
|
130
|
-
|
131
|
-
def execute
|
132
|
-
profiledirectory = mappingfile = pstorefile = nil
|
133
|
-
existancecheck = false
|
134
|
-
begin
|
135
|
-
opt = GetoptLong.new(
|
136
|
-
["-d","--directory", GetoptLong::REQUIRED_ARGUMENT],
|
137
|
-
["-f","--file", GetoptLong::REQUIRED_ARGUMENT],
|
138
|
-
["-c","--check", GetoptLong::NO_ARGUMENT],
|
139
|
-
["-h","--help", GetoptLong::NO_ARGUMENT],
|
140
|
-
["-w","--wurfldb", GetoptLong::REQUIRED_ARGUMENT]
|
141
|
-
)
|
142
|
-
|
143
|
-
opt.each { |arg,val|
|
144
|
-
case arg
|
145
|
-
when "-d"
|
146
|
-
profiledirectory = val.strip
|
147
|
-
when "-f"
|
148
|
-
mappingfile = val.strip
|
149
|
-
when "-c"
|
150
|
-
existancecheck = true
|
151
|
-
when "-h"
|
152
|
-
help
|
153
|
-
when "-w"
|
154
|
-
pstorefile = val.strip
|
155
|
-
else
|
156
|
-
usage
|
157
|
-
end
|
158
|
-
}
|
159
|
-
usage if mappingfile.nil? || profiledirectory.nil?
|
160
|
-
rescue => err
|
161
|
-
usage
|
162
|
-
end
|
163
|
-
|
164
|
-
profiles = Hash.new
|
165
|
-
duplicates = Hash.new
|
166
|
-
mappings = parse_mapping_file(mappingfile)
|
167
|
-
mappings.each do |uaprof,useragent|
|
168
|
-
begin
|
169
|
-
prof_file = get_uaprofile(uaprof,profiledirectory,existancecheck)
|
170
|
-
uaprof_mapper = UAProfToWURLF.new
|
171
|
-
if profiles.key?(useragent)
|
172
|
-
duplicates[useragent] = Array.new if !duplicates.key?(useragent)
|
173
|
-
duplicates[useragent]<<uaprof
|
174
|
-
next
|
175
|
-
end
|
176
|
-
uaprof_mapper.parse_UAProf("#{profiledirectory}/#{prof_file}")
|
177
|
-
profiles[useragent] = uaprof_mapper
|
178
|
-
rescue => err
|
179
|
-
$stderr.puts "Error: File #{prof_file}; User-Agent:#{useragent}"
|
180
|
-
$stderr.puts "Error:#{err.message}"
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
duplicates.each do |key,profs|
|
185
|
-
$stderr.puts "Duplicates exist for #{key}"
|
186
|
-
profs.each {|prof| $stderr.puts "-- #{prof}" }
|
187
|
-
end
|
188
|
-
|
189
|
-
exit 0 if !pstorefile
|
190
|
-
|
191
|
-
wurflhandsets = load_pstore(pstorefile)
|
192
|
-
|
193
|
-
puts "Comparing WURFL Handsets"
|
194
|
-
profiles.each do |key,val|
|
195
|
-
puts "",""
|
196
|
-
|
197
|
-
if !wurflhandsets.key?(key)
|
198
|
-
puts "UAProf has a new Handset: #{key}"
|
199
|
-
puts "--------------------------------"
|
200
|
-
val.output_WURFL
|
201
|
-
puts "--------------------------------"
|
202
|
-
else
|
203
|
-
uahand = val.make_wurfl_handset
|
204
|
-
res = uahand.compare(wurflhandsets[key])
|
205
|
-
if res.size > 0
|
206
|
-
puts "#{key} : For UAProf and WURFL differ"
|
207
|
-
res.each do |dkey,dval,did|
|
208
|
-
next if did == "generic"
|
209
|
-
#Key UAPROF Value WURFL Value WURFL source id
|
210
|
-
puts " Key:#{dkey}; UVAL:#{uahand[dkey]}; WVAL:#{dval}; WSRCID:#{did}"
|
211
|
-
end
|
212
|
-
#val["user_agent"] = key
|
213
|
-
puts ""
|
214
|
-
puts "WURFL Changes are:"
|
215
|
-
puts ""
|
216
|
-
val.output_WURFL(res.map {|entry| entry[0]})
|
217
|
-
else
|
218
|
-
puts "#{key} : For UAProf and WURFL match"
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|