716 |
02 Jun 08 |
nicklas |
1 |
/** |
716 |
02 Jun 08 |
nicklas |
$Id $ |
716 |
02 Jun 08 |
nicklas |
3 |
|
716 |
02 Jun 08 |
nicklas |
Copyright (C) 2008 Nicklas Nordborg |
716 |
02 Jun 08 |
nicklas |
5 |
|
1381 |
15 Aug 11 |
martin |
This file is part of the FTP Server extension for BASE. |
716 |
02 Jun 08 |
nicklas |
Available at http://baseplugins.thep.lu.se/ |
1381 |
15 Aug 11 |
martin |
BASE main site: http://base.thep.lu.se/ |
1381 |
15 Aug 11 |
martin |
9 |
----------------------------------------------------------- |
1381 |
15 Aug 11 |
martin |
10 |
|
1381 |
15 Aug 11 |
martin |
This is free software; you can redistribute it and/or |
716 |
02 Jun 08 |
nicklas |
modify it under the terms of the GNU General Public License |
1381 |
15 Aug 11 |
martin |
as published by the Free Software Foundation; either version 3 |
716 |
02 Jun 08 |
nicklas |
of the License, or (at your option) any later version. |
1381 |
15 Aug 11 |
martin |
15 |
|
1381 |
15 Aug 11 |
martin |
The software is distributed in the hope that it will be useful, |
716 |
02 Jun 08 |
nicklas |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
716 |
02 Jun 08 |
nicklas |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
716 |
02 Jun 08 |
nicklas |
GNU General Public License for more details. |
1381 |
15 Aug 11 |
martin |
20 |
|
1381 |
15 Aug 11 |
martin |
You should have received a copy of the GNU General Public License |
1381 |
15 Aug 11 |
martin |
along with BASE. If not, see <http://www.gnu.org/licenses/>. |
716 |
02 Jun 08 |
nicklas |
23 |
|
716 |
02 Jun 08 |
nicklas |
24 |
*/ |
714 |
30 May 08 |
nicklas |
25 |
package net.sf.basedb.clients.ftp; |
714 |
30 May 08 |
nicklas |
26 |
|
714 |
30 May 08 |
nicklas |
27 |
import net.sf.basedb.core.DbControl; |
714 |
30 May 08 |
nicklas |
28 |
import net.sf.basedb.core.Directory; |
714 |
30 May 08 |
nicklas |
29 |
import net.sf.basedb.core.File; |
714 |
30 May 08 |
nicklas |
30 |
import net.sf.basedb.core.Path; |
714 |
30 May 08 |
nicklas |
31 |
|
915 |
11 Dec 08 |
nicklas |
32 |
import org.apache.ftpserver.ftplet.FtpFile; |
714 |
30 May 08 |
nicklas |
33 |
import org.apache.ftpserver.ftplet.FileSystemView; |
714 |
30 May 08 |
nicklas |
34 |
import org.apache.ftpserver.ftplet.FtpException; |
716 |
02 Jun 08 |
nicklas |
35 |
import org.slf4j.Logger; |
716 |
02 Jun 08 |
nicklas |
36 |
import org.slf4j.LoggerFactory; |
714 |
30 May 08 |
nicklas |
37 |
|
714 |
30 May 08 |
nicklas |
38 |
/** |
714 |
30 May 08 |
nicklas |
The view of the BASE file system as seen from the currently logged in user. |
714 |
30 May 08 |
nicklas |
Users will start out in their home directory if they have one, otherwise in |
714 |
30 May 08 |
nicklas |
the root. |
716 |
02 Jun 08 |
nicklas |
42 |
|
716 |
02 Jun 08 |
nicklas |
@author Nicklas |
716 |
02 Jun 08 |
nicklas |
@version 1.0 |
714 |
30 May 08 |
nicklas |
45 |
*/ |
714 |
30 May 08 |
nicklas |
46 |
public class BaseFileSystemView |
714 |
30 May 08 |
nicklas |
47 |
implements FileSystemView |
714 |
30 May 08 |
nicklas |
48 |
{ |
714 |
30 May 08 |
nicklas |
49 |
|
716 |
02 Jun 08 |
nicklas |
50 |
private static final Logger log = LoggerFactory.getLogger(BaseFileSystemView.class); |
716 |
02 Jun 08 |
nicklas |
51 |
|
714 |
30 May 08 |
nicklas |
52 |
private BaseUser user; |
915 |
11 Dec 08 |
nicklas |
53 |
private BaseFtpFile currentDir; |
714 |
30 May 08 |
nicklas |
54 |
|
716 |
02 Jun 08 |
nicklas |
55 |
/** |
716 |
02 Jun 08 |
nicklas |
Create a new file system view for the specified user. |
716 |
02 Jun 08 |
nicklas |
57 |
*/ |
714 |
30 May 08 |
nicklas |
58 |
public BaseFileSystemView(BaseUser user) |
714 |
30 May 08 |
nicklas |
59 |
{ |
714 |
30 May 08 |
nicklas |
60 |
this.user = user; |
714 |
30 May 08 |
nicklas |
61 |
this.currentDir = user.getHome(); |
714 |
30 May 08 |
nicklas |
62 |
} |
714 |
30 May 08 |
nicklas |
63 |
|
714 |
30 May 08 |
nicklas |
64 |
/* |
714 |
30 May 08 |
nicklas |
From the FileSystemView interface |
714 |
30 May 08 |
nicklas |
66 |
--------------------------------------- |
714 |
30 May 08 |
nicklas |
67 |
*/ |
714 |
30 May 08 |
nicklas |
68 |
|
714 |
30 May 08 |
nicklas |
69 |
@Override |
915 |
11 Dec 08 |
nicklas |
70 |
public FtpFile getWorkingDirectory() |
714 |
30 May 08 |
nicklas |
71 |
throws FtpException |
714 |
30 May 08 |
nicklas |
72 |
{ |
716 |
02 Jun 08 |
nicklas |
73 |
if (log.isDebugEnabled()) log.debug("Current directory: " + currentDir); |
714 |
30 May 08 |
nicklas |
74 |
return currentDir; |
714 |
30 May 08 |
nicklas |
75 |
} |
714 |
30 May 08 |
nicklas |
76 |
|
714 |
30 May 08 |
nicklas |
77 |
/** |
714 |
30 May 08 |
nicklas |
Change to another current directory. There are three cases: |
714 |
30 May 08 |
nicklas |
<ol> |
714 |
30 May 08 |
nicklas |
<li>The path is '..', meaning that we have to go up one |
714 |
30 May 08 |
nicklas |
directory, unless we are at the root directory. |
714 |
30 May 08 |
nicklas |
<li>The path starts with a '/', meaning that it is an absolute |
714 |
30 May 08 |
nicklas |
path |
1229 |
19 Aug 10 |
nicklas |
<li>The path is '.', meaning that we stay in the current |
1229 |
19 Aug 10 |
nicklas |
directory (this is used by some clients to keep the |
1229 |
19 Aug 10 |
nicklas |
connection alive) |
714 |
30 May 08 |
nicklas |
<li>All other paths are relative to the current directory. |
714 |
30 May 08 |
nicklas |
</ol> |
714 |
30 May 08 |
nicklas |
89 |
*/ |
714 |
30 May 08 |
nicklas |
90 |
@Override |
915 |
11 Dec 08 |
nicklas |
91 |
public boolean changeWorkingDirectory(String path) |
714 |
30 May 08 |
nicklas |
92 |
throws FtpException |
714 |
30 May 08 |
nicklas |
93 |
{ |
716 |
02 Jun 08 |
nicklas |
94 |
if (log.isDebugEnabled()) |
716 |
02 Jun 08 |
nicklas |
95 |
{ |
716 |
02 Jun 08 |
nicklas |
96 |
log.debug("Change directory"); |
716 |
02 Jun 08 |
nicklas |
97 |
log.debug(" path=" + path); |
716 |
02 Jun 08 |
nicklas |
98 |
log.debug(" current="+currentDir); |
716 |
02 Jun 08 |
nicklas |
99 |
log.debug(" user=" + user); |
716 |
02 Jun 08 |
nicklas |
100 |
} |
714 |
30 May 08 |
nicklas |
101 |
Path changeTo = null; |
714 |
30 May 08 |
nicklas |
102 |
if (path.startsWith("/")) |
714 |
30 May 08 |
nicklas |
103 |
{ |
714 |
30 May 08 |
nicklas |
// Absolute path |
714 |
30 May 08 |
nicklas |
105 |
changeTo = new Path(append(path, ""), Path.Type.DIRECTORY); |
714 |
30 May 08 |
nicklas |
106 |
} |
714 |
30 May 08 |
nicklas |
107 |
else if (path.startsWith("..")) |
714 |
30 May 08 |
nicklas |
108 |
{ |
714 |
30 May 08 |
nicklas |
// Move to parent directory |
915 |
11 Dec 08 |
nicklas |
110 |
changeTo = new Path(currentDir.getAbsolutePath(), Path.Type.DIRECTORY).getParent(); |
714 |
30 May 08 |
nicklas |
111 |
} |
1229 |
19 Aug 10 |
nicklas |
112 |
else if (path.equals(".")) |
1229 |
19 Aug 10 |
nicklas |
113 |
{ |
1229 |
19 Aug 10 |
nicklas |
// Keep changeTo == null |
1229 |
19 Aug 10 |
nicklas |
115 |
} |
714 |
30 May 08 |
nicklas |
116 |
else |
714 |
30 May 08 |
nicklas |
117 |
{ |
714 |
30 May 08 |
nicklas |
// Move to subdir within current directory |
915 |
11 Dec 08 |
nicklas |
119 |
changeTo = new Path(append(currentDir.getAbsolutePath(), path), Path.Type.DIRECTORY); |
714 |
30 May 08 |
nicklas |
120 |
} |
716 |
02 Jun 08 |
nicklas |
121 |
if (log.isDebugEnabled()) |
716 |
02 Jun 08 |
nicklas |
122 |
{ |
716 |
02 Jun 08 |
nicklas |
123 |
log.debug(" to="+changeTo); |
716 |
02 Jun 08 |
nicklas |
124 |
} |
714 |
30 May 08 |
nicklas |
125 |
DbControl dc = user.sc.newDbControl(); |
714 |
30 May 08 |
nicklas |
126 |
try |
714 |
30 May 08 |
nicklas |
127 |
{ |
1229 |
19 Aug 10 |
nicklas |
128 |
if (changeTo != null) |
1229 |
19 Aug 10 |
nicklas |
129 |
{ |
1229 |
19 Aug 10 |
nicklas |
130 |
currentDir = new BaseFtpFile(user, Directory.getByPath(dc, changeTo)); |
1229 |
19 Aug 10 |
nicklas |
131 |
} |
716 |
02 Jun 08 |
nicklas |
132 |
log.debug("Directory changed to: " + currentDir); |
714 |
30 May 08 |
nicklas |
133 |
return true; |
714 |
30 May 08 |
nicklas |
134 |
} |
714 |
30 May 08 |
nicklas |
135 |
catch (RuntimeException ex) |
714 |
30 May 08 |
nicklas |
136 |
{ |
716 |
02 Jun 08 |
nicklas |
137 |
log.error("Could not change to directory: " + path, ex); |
714 |
30 May 08 |
nicklas |
138 |
return false; |
714 |
30 May 08 |
nicklas |
139 |
} |
714 |
30 May 08 |
nicklas |
140 |
finally |
714 |
30 May 08 |
nicklas |
141 |
{ |
714 |
30 May 08 |
nicklas |
142 |
if (dc != null) dc.close(); |
714 |
30 May 08 |
nicklas |
143 |
} |
714 |
30 May 08 |
nicklas |
144 |
} |
714 |
30 May 08 |
nicklas |
145 |
|
714 |
30 May 08 |
nicklas |
146 |
/** |
714 |
30 May 08 |
nicklas |
Do nothing. |
714 |
30 May 08 |
nicklas |
148 |
*/ |
714 |
30 May 08 |
nicklas |
149 |
@Override |
714 |
30 May 08 |
nicklas |
150 |
public void dispose() |
714 |
30 May 08 |
nicklas |
151 |
{} |
714 |
30 May 08 |
nicklas |
152 |
|
714 |
30 May 08 |
nicklas |
153 |
/** |
714 |
30 May 08 |
nicklas |
Always true. |
714 |
30 May 08 |
nicklas |
155 |
*/ |
714 |
30 May 08 |
nicklas |
156 |
@Override |
714 |
30 May 08 |
nicklas |
157 |
public boolean isRandomAccessible() |
714 |
30 May 08 |
nicklas |
158 |
throws FtpException |
714 |
30 May 08 |
nicklas |
159 |
{ |
714 |
30 May 08 |
nicklas |
160 |
return true; |
714 |
30 May 08 |
nicklas |
161 |
} |
714 |
30 May 08 |
nicklas |
162 |
|
714 |
30 May 08 |
nicklas |
163 |
/** |
714 |
30 May 08 |
nicklas |
Get a file object. There are three cases regarding the file name: |
714 |
30 May 08 |
nicklas |
<ul> |
714 |
30 May 08 |
nicklas |
<li>It starts with './'. The path is relative the current directory |
714 |
30 May 08 |
nicklas |
but we have to remove the './'. |
714 |
30 May 08 |
nicklas |
<li>It starts with '/'. It is an absolute path. |
714 |
30 May 08 |
nicklas |
<li>Otherwise it is relative to the current directory. |
714 |
30 May 08 |
nicklas |
</ul> |
714 |
30 May 08 |
nicklas |
171 |
|
714 |
30 May 08 |
nicklas |
Once we have the full path, we first check BASE if there is an existing |
714 |
30 May 08 |
nicklas |
file with the name. If so, we create a file object representing that file. |
714 |
30 May 08 |
nicklas |
Otherwise we check if there is a directory with the name and create a file |
714 |
30 May 08 |
nicklas |
object representing that directory. As a last resort we create a file object |
714 |
30 May 08 |
nicklas |
representing a non-existing file or directory. |
714 |
30 May 08 |
nicklas |
177 |
*/ |
714 |
30 May 08 |
nicklas |
178 |
@Override |
915 |
11 Dec 08 |
nicklas |
179 |
public FtpFile getFile(String fileName) |
714 |
30 May 08 |
nicklas |
180 |
throws FtpException |
714 |
30 May 08 |
nicklas |
181 |
{ |
716 |
02 Jun 08 |
nicklas |
182 |
if (log.isDebugEnabled()) |
716 |
02 Jun 08 |
nicklas |
183 |
{ |
716 |
02 Jun 08 |
nicklas |
184 |
log.debug("Get file object"); |
716 |
02 Jun 08 |
nicklas |
185 |
log.debug(" name=" + fileName); |
716 |
02 Jun 08 |
nicklas |
186 |
log.debug(" current="+currentDir); |
716 |
02 Jun 08 |
nicklas |
187 |
log.debug(" user=" + user); |
716 |
02 Jun 08 |
nicklas |
188 |
} |
717 |
03 Jun 08 |
nicklas |
189 |
boolean preferDirectory = fileName.endsWith("/"); |
714 |
30 May 08 |
nicklas |
190 |
if (fileName.startsWith("./")) |
714 |
30 May 08 |
nicklas |
191 |
{ |
915 |
11 Dec 08 |
nicklas |
192 |
fileName = append(currentDir.getAbsolutePath(), fileName.substring(2)); |
714 |
30 May 08 |
nicklas |
193 |
} |
714 |
30 May 08 |
nicklas |
194 |
else if (!fileName.startsWith("/")) |
714 |
30 May 08 |
nicklas |
195 |
{ |
915 |
11 Dec 08 |
nicklas |
196 |
fileName = append(currentDir.getAbsolutePath(), fileName); |
714 |
30 May 08 |
nicklas |
197 |
} |
714 |
30 May 08 |
nicklas |
198 |
|
915 |
11 Dec 08 |
nicklas |
199 |
BaseFtpFile fo = null; |
714 |
30 May 08 |
nicklas |
200 |
DbControl dc = user.sc.newDbControl(); |
714 |
30 May 08 |
nicklas |
201 |
try |
714 |
30 May 08 |
nicklas |
202 |
{ |
716 |
02 Jun 08 |
nicklas |
203 |
if (log.isDebugEnabled()) |
716 |
02 Jun 08 |
nicklas |
204 |
{ |
716 |
02 Jun 08 |
nicklas |
205 |
log.debug(" full path=" + fileName); |
716 |
02 Jun 08 |
nicklas |
206 |
} |
716 |
02 Jun 08 |
nicklas |
207 |
|
716 |
02 Jun 08 |
nicklas |
// Check if the path represents a file |
717 |
03 Jun 08 |
nicklas |
209 |
if (preferDirectory) fo = getDirectory(dc, fileName); |
717 |
03 Jun 08 |
nicklas |
210 |
if (fo == null) fo = getFile(dc, fileName); |
717 |
03 Jun 08 |
nicklas |
211 |
if (fo == null && !preferDirectory) fo = getDirectory(dc, fileName); |
714 |
30 May 08 |
nicklas |
212 |
} |
714 |
30 May 08 |
nicklas |
213 |
finally |
714 |
30 May 08 |
nicklas |
214 |
{ |
714 |
30 May 08 |
nicklas |
215 |
if (dc != null) dc.close(); |
714 |
30 May 08 |
nicklas |
216 |
} |
915 |
11 Dec 08 |
nicklas |
217 |
if (fo == null) fo = new BaseFtpFile(user, fileName); |
716 |
02 Jun 08 |
nicklas |
218 |
if (log.isDebugEnabled()) |
716 |
02 Jun 08 |
nicklas |
219 |
{ |
716 |
02 Jun 08 |
nicklas |
220 |
log.debug("Get file object: " + fo); |
716 |
02 Jun 08 |
nicklas |
221 |
} |
714 |
30 May 08 |
nicklas |
222 |
return fo; |
714 |
30 May 08 |
nicklas |
223 |
} |
714 |
30 May 08 |
nicklas |
224 |
|
714 |
30 May 08 |
nicklas |
225 |
@Override |
915 |
11 Dec 08 |
nicklas |
226 |
public FtpFile getHomeDirectory() |
714 |
30 May 08 |
nicklas |
227 |
throws FtpException |
714 |
30 May 08 |
nicklas |
228 |
{ |
714 |
30 May 08 |
nicklas |
229 |
return user.getHome(); |
714 |
30 May 08 |
nicklas |
230 |
} |
716 |
02 Jun 08 |
nicklas |
231 |
// ---------------------------------------- |
714 |
30 May 08 |
nicklas |
232 |
|
714 |
30 May 08 |
nicklas |
233 |
/** |
717 |
03 Jun 08 |
nicklas |
Try to load the given path as a BASE File object. |
717 |
03 Jun 08 |
nicklas |
235 |
*/ |
915 |
11 Dec 08 |
nicklas |
236 |
private BaseFtpFile getFile(DbControl dc, String path) |
717 |
03 Jun 08 |
nicklas |
237 |
{ |
915 |
11 Dec 08 |
nicklas |
238 |
BaseFtpFile fo = null; |
717 |
03 Jun 08 |
nicklas |
239 |
try |
717 |
03 Jun 08 |
nicklas |
240 |
{ |
717 |
03 Jun 08 |
nicklas |
241 |
Path filePath = new Path(path, Path.Type.FILE); |
717 |
03 Jun 08 |
nicklas |
242 |
File file = File.getByPath(dc, filePath, false); |
915 |
11 Dec 08 |
nicklas |
243 |
fo = new BaseFtpFile(user, file); |
717 |
03 Jun 08 |
nicklas |
244 |
} |
717 |
03 Jun 08 |
nicklas |
245 |
catch (Exception ex) |
717 |
03 Jun 08 |
nicklas |
246 |
{ |
717 |
03 Jun 08 |
nicklas |
247 |
if (log.isDebugEnabled()) |
717 |
03 Jun 08 |
nicklas |
248 |
{ |
717 |
03 Jun 08 |
nicklas |
249 |
log.debug("Not a file: " + path, ex); |
717 |
03 Jun 08 |
nicklas |
250 |
} |
717 |
03 Jun 08 |
nicklas |
251 |
} |
717 |
03 Jun 08 |
nicklas |
252 |
return fo; |
717 |
03 Jun 08 |
nicklas |
253 |
} |
717 |
03 Jun 08 |
nicklas |
254 |
|
717 |
03 Jun 08 |
nicklas |
255 |
/** |
717 |
03 Jun 08 |
nicklas |
Try to load the given path as a BASE Directory object. |
717 |
03 Jun 08 |
nicklas |
257 |
*/ |
915 |
11 Dec 08 |
nicklas |
258 |
private BaseFtpFile getDirectory(DbControl dc, String path) |
717 |
03 Jun 08 |
nicklas |
259 |
{ |
915 |
11 Dec 08 |
nicklas |
260 |
BaseFtpFile fo = null; |
717 |
03 Jun 08 |
nicklas |
261 |
try |
717 |
03 Jun 08 |
nicklas |
262 |
{ |
717 |
03 Jun 08 |
nicklas |
263 |
Path dirPath = new Path(path, Path.Type.DIRECTORY); |
717 |
03 Jun 08 |
nicklas |
264 |
Directory dir = Directory.getByPath(dc, dirPath); |
915 |
11 Dec 08 |
nicklas |
265 |
fo = new BaseFtpFile(user, dir); |
717 |
03 Jun 08 |
nicklas |
266 |
} |
717 |
03 Jun 08 |
nicklas |
267 |
catch (Exception ex) |
717 |
03 Jun 08 |
nicklas |
268 |
{ |
717 |
03 Jun 08 |
nicklas |
269 |
if (log.isDebugEnabled()) |
717 |
03 Jun 08 |
nicklas |
270 |
{ |
717 |
03 Jun 08 |
nicklas |
271 |
log.debug("Not a directory: " + path, ex); |
717 |
03 Jun 08 |
nicklas |
272 |
} |
717 |
03 Jun 08 |
nicklas |
273 |
} |
717 |
03 Jun 08 |
nicklas |
274 |
return fo; |
717 |
03 Jun 08 |
nicklas |
275 |
} |
717 |
03 Jun 08 |
nicklas |
276 |
|
717 |
03 Jun 08 |
nicklas |
277 |
|
717 |
03 Jun 08 |
nicklas |
278 |
/** |
714 |
30 May 08 |
nicklas |
Concatenate two paths to create a full path. The concatenation makes |
714 |
30 May 08 |
nicklas |
sure that no '/' is missing or doubled. The path never ends with '/' |
714 |
30 May 08 |
nicklas |
unless it is the root directory. |
714 |
30 May 08 |
nicklas |
282 |
*/ |
714 |
30 May 08 |
nicklas |
283 |
private String append(String path1, String path2) |
714 |
30 May 08 |
nicklas |
284 |
{ |
714 |
30 May 08 |
nicklas |
285 |
boolean atEnd = path1.endsWith("/"); |
714 |
30 May 08 |
nicklas |
286 |
boolean atStart = path2.startsWith("/"); |
714 |
30 May 08 |
nicklas |
287 |
|
714 |
30 May 08 |
nicklas |
288 |
String result = null; |
714 |
30 May 08 |
nicklas |
289 |
if (atEnd && atStart) |
714 |
30 May 08 |
nicklas |
290 |
{ |
714 |
30 May 08 |
nicklas |
291 |
result = path1 + path2.substring(1); |
714 |
30 May 08 |
nicklas |
292 |
} |
714 |
30 May 08 |
nicklas |
293 |
else if (atEnd || atStart) |
714 |
30 May 08 |
nicklas |
294 |
{ |
714 |
30 May 08 |
nicklas |
295 |
result = path1 + path2; |
714 |
30 May 08 |
nicklas |
296 |
} |
714 |
30 May 08 |
nicklas |
297 |
else |
714 |
30 May 08 |
nicklas |
298 |
{ |
714 |
30 May 08 |
nicklas |
299 |
result = path1 + "/" + path2; |
714 |
30 May 08 |
nicklas |
300 |
} |
714 |
30 May 08 |
nicklas |
301 |
|
714 |
30 May 08 |
nicklas |
302 |
if (result.endsWith("/") && result.length() > 1) |
714 |
30 May 08 |
nicklas |
303 |
{ |
714 |
30 May 08 |
nicklas |
304 |
result = result.substring(0, result.length() - 1); |
714 |
30 May 08 |
nicklas |
305 |
} |
716 |
02 Jun 08 |
nicklas |
306 |
if (log.isDebugEnabled()) |
716 |
02 Jun 08 |
nicklas |
307 |
{ |
716 |
02 Jun 08 |
nicklas |
308 |
log.debug("append: path1=" + path1 + "; path2=" + path2 + "; result=" + result); |
716 |
02 Jun 08 |
nicklas |
309 |
} |
714 |
30 May 08 |
nicklas |
310 |
return result; |
714 |
30 May 08 |
nicklas |
311 |
} |
714 |
30 May 08 |
nicklas |
312 |
|
714 |
30 May 08 |
nicklas |
313 |
} |