1 |
|
|
2 |
|
|
3 |
|
|
4 |
|
|
5 |
|
|
6 |
|
|
7 |
|
|
8 |
|
|
9 |
|
|
10 |
|
|
11 |
|
|
12 |
|
|
13 |
|
|
14 |
|
|
15 |
|
|
16 |
|
|
17 |
|
package org.apache.jetspeed.security.spi.impl.ldap; |
18 |
|
|
19 |
|
import java.security.Principal; |
20 |
|
import java.util.ArrayList; |
21 |
|
import java.util.Enumeration; |
22 |
|
import java.util.Iterator; |
23 |
|
import java.util.List; |
24 |
|
|
25 |
|
import javax.naming.NamingEnumeration; |
26 |
|
import javax.naming.NamingException; |
27 |
|
import javax.naming.directory.Attribute; |
28 |
|
import javax.naming.directory.Attributes; |
29 |
|
import javax.naming.directory.BasicAttribute; |
30 |
|
import javax.naming.directory.BasicAttributes; |
31 |
|
import javax.naming.directory.DirContext; |
32 |
|
import javax.naming.directory.SearchControls; |
33 |
|
import javax.naming.directory.SearchResult; |
34 |
|
|
35 |
|
import org.apache.commons.lang.StringUtils; |
36 |
|
import org.apache.commons.logging.Log; |
37 |
|
import org.apache.commons.logging.LogFactory; |
38 |
|
import org.apache.jetspeed.security.SecurityException; |
39 |
|
import org.apache.jetspeed.security.impl.UserPrincipalImpl; |
40 |
|
|
41 |
|
|
42 |
|
public class LdapMemberShipDaoImpl extends LdapPrincipalDaoImpl implements LdapMembershipDao { |
43 |
|
|
44 |
|
|
45 |
0 |
private static final Log logger = LogFactory.getLog(LdapMemberShipDaoImpl.class); |
46 |
|
|
47 |
|
public LdapMemberShipDaoImpl() throws SecurityException { |
48 |
0 |
super(); |
49 |
0 |
} |
50 |
|
|
51 |
|
public LdapMemberShipDaoImpl(LdapBindingConfig config) throws SecurityException { |
52 |
0 |
super(config); |
53 |
0 |
} |
54 |
|
|
55 |
|
|
56 |
|
|
57 |
|
|
58 |
|
public String[] searchGroupMemberShipByGroup(final String userPrincipalUid, SearchControls cons) throws NamingException { |
59 |
|
|
60 |
0 |
String query = "(&(" + getGroupMembershipAttribute() + "=" + getUserDN(userPrincipalUid) + ")" + getGroupFilter() + ")"; |
61 |
|
|
62 |
0 |
if (logger.isDebugEnabled()) |
63 |
|
{ |
64 |
0 |
logger.debug("query[" + query + "]"); |
65 |
|
} |
66 |
|
|
67 |
0 |
cons.setSearchScope(getSearchScope()); |
68 |
0 |
String groupFilterBase = getGroupFilterBase(); |
69 |
0 |
NamingEnumeration searchResults = ((DirContext) ctx).search(groupFilterBase,query , cons); |
70 |
|
|
71 |
0 |
List groupPrincipalUids = new ArrayList(); |
72 |
0 |
while (searchResults.hasMore()) |
73 |
|
{ |
74 |
0 |
SearchResult result = (SearchResult) searchResults.next(); |
75 |
0 |
Attributes answer = result.getAttributes(); |
76 |
0 |
groupPrincipalUids.addAll(getAttributes(getAttribute(getGroupIdAttribute(), answer))); |
77 |
0 |
} |
78 |
0 |
return (String[]) groupPrincipalUids.toArray(new String[groupPrincipalUids.size()]); |
79 |
|
|
80 |
|
} |
81 |
|
|
82 |
|
|
83 |
|
|
84 |
|
|
85 |
|
public String[] searchGroupMemberShipByUser(final String userPrincipalUid, SearchControls cons) throws NamingException { |
86 |
0 |
NamingEnumeration searchResults = searchByWildcardedUid(userPrincipalUid, cons); |
87 |
|
|
88 |
0 |
if (!searchResults.hasMore()) |
89 |
|
{ |
90 |
0 |
throw new NamingException("Could not find any user with uid[" + userPrincipalUid + "]"); |
91 |
|
} |
92 |
|
|
93 |
0 |
Attributes userAttributes = getFirstUser(searchResults); |
94 |
0 |
List groupUids = new ArrayList(); |
95 |
0 |
Attribute attr = getAttribute(getUserGroupMembershipAttribute(), userAttributes); |
96 |
0 |
List attrs = getAttributes(attr); |
97 |
0 |
Iterator it = attrs.iterator(); |
98 |
0 |
while(it.hasNext()) { |
99 |
0 |
String cnfull = (String)it.next(); |
100 |
0 |
if(cnfull.toLowerCase().indexOf(getGroupFilterBase().toLowerCase())!=-1) { |
101 |
0 |
String cn = extractLdapAttr(cnfull,getRoleUidAttribute()); |
102 |
0 |
if (cn != null){ |
103 |
0 |
groupUids.add(cn); |
104 |
|
} |
105 |
|
} |
106 |
0 |
} |
107 |
|
|
108 |
0 |
return (String[]) groupUids.toArray(new String[groupUids.size()]); |
109 |
|
} |
110 |
|
|
111 |
|
|
112 |
|
|
113 |
|
|
114 |
|
public String[] searchRoleMemberShipByRole(final String userPrincipalUid, SearchControls cons) throws NamingException { |
115 |
|
|
116 |
0 |
String query = "(&(" + getRoleMembershipAttribute() + "=" + getUserDN(userPrincipalUid) + ")" + getRoleFilter() + ")"; |
117 |
|
|
118 |
0 |
if (logger.isDebugEnabled()) |
119 |
|
{ |
120 |
0 |
logger.debug("query[" + query + "]"); |
121 |
|
} |
122 |
|
|
123 |
0 |
cons.setSearchScope(getSearchScope()); |
124 |
0 |
NamingEnumeration searchResults = ((DirContext) ctx).search(getRoleFilterBase(),query , cons); |
125 |
0 |
List rolePrincipalUids = new ArrayList(); |
126 |
0 |
while (searchResults.hasMore()) |
127 |
|
{ |
128 |
|
|
129 |
0 |
SearchResult result = (SearchResult) searchResults.next(); |
130 |
0 |
Attributes answer = result.getAttributes(); |
131 |
0 |
rolePrincipalUids.addAll(getAttributes(getAttribute(getRoleIdAttribute(), answer))); |
132 |
0 |
} |
133 |
0 |
return (String[]) rolePrincipalUids.toArray(new String[rolePrincipalUids.size()]); |
134 |
|
} |
135 |
|
|
136 |
|
|
137 |
|
|
138 |
|
|
139 |
|
public String[] searchRoleMemberShipByUser(final String userPrincipalUid, SearchControls cons) throws NamingException { |
140 |
|
|
141 |
0 |
NamingEnumeration results = searchByWildcardedUid(userPrincipalUid, cons); |
142 |
|
|
143 |
0 |
if (!results.hasMore()) |
144 |
|
{ |
145 |
0 |
throw new NamingException("Could not find any user with uid[" + userPrincipalUid + "]"); |
146 |
|
} |
147 |
|
|
148 |
0 |
Attributes userAttributes = getFirstUser(results); |
149 |
0 |
List newAttrs = new ArrayList(); |
150 |
0 |
Attribute attr = getAttribute(getUserRoleMembershipAttribute(), userAttributes); |
151 |
0 |
List attrs = getAttributes(attr); |
152 |
0 |
Iterator it = attrs.iterator(); |
153 |
0 |
while(it.hasNext()) { |
154 |
0 |
String cnfull = (String)it.next(); |
155 |
0 |
if(cnfull.toLowerCase().indexOf(getRoleFilterBase().toLowerCase())!=-1) { |
156 |
0 |
String cn = extractLdapAttr(cnfull,getRoleUidAttribute()); |
157 |
0 |
if (cn != null){ |
158 |
0 |
newAttrs.add(cn); |
159 |
|
} |
160 |
0 |
}else{ |
161 |
|
|
162 |
0 |
String cn = cnfull; |
163 |
0 |
newAttrs.add(cn); |
164 |
|
} |
165 |
0 |
} |
166 |
0 |
return (String[]) newAttrs.toArray(new String[class="keyword">newAttrs.size()]); |
167 |
|
} |
168 |
|
|
169 |
|
|
170 |
|
|
171 |
|
|
172 |
|
public String[] searchUsersFromGroupByGroup(final String groupPrincipalUid, SearchControls cons) |
173 |
|
throws NamingException |
174 |
|
{ |
175 |
|
|
176 |
0 |
String query = "(&(" + getGroupIdAttribute() + "=" + (groupPrincipalUid) + ")" + getGroupFilter() + ")"; |
177 |
|
|
178 |
0 |
if (logger.isDebugEnabled()) |
179 |
|
{ |
180 |
0 |
logger.debug("query[" + query + "]"); |
181 |
|
} |
182 |
|
|
183 |
0 |
ArrayList userPrincipalUids=new ArrayList(); |
184 |
|
|
185 |
0 |
cons.setSearchScope(getSearchScope()); |
186 |
0 |
NamingEnumeration results = ((DirContext) ctx).search(getGroupFilterBase(),query , cons); |
187 |
|
|
188 |
0 |
while (results.hasMore()) |
189 |
|
{ |
190 |
0 |
SearchResult result = (SearchResult) results.next(); |
191 |
0 |
Attributes answer = result.getAttributes(); |
192 |
|
|
193 |
0 |
List newAttrs = new ArrayList(); |
194 |
|
|
195 |
0 |
Attribute userPrincipalUid = getAttribute(getGroupMembershipAttribute(), answer); |
196 |
0 |
List attrs = getAttributes(userPrincipalUid); |
197 |
0 |
Iterator it = attrs.iterator(); |
198 |
0 |
while(it.hasNext()) { |
199 |
0 |
String uidfull = (String)it.next(); |
200 |
0 |
if (!StringUtils.isEmpty(uidfull)) { |
201 |
0 |
if (uidfull.toLowerCase().indexOf(getUserFilterBase().toLowerCase())!=-1) { |
202 |
0 |
String uid = extractLdapAttr(uidfull,getUserIdAttribute()); |
203 |
0 |
if (uid != null){ |
204 |
0 |
newAttrs.add(uid); |
205 |
|
} |
206 |
|
} |
207 |
|
} |
208 |
0 |
} |
209 |
0 |
userPrincipalUids.addAll(newAttrs); |
210 |
0 |
} |
211 |
0 |
return (String[]) userPrincipalUids.toArray(new String[userPrincipalUids.size()]); |
212 |
|
} |
213 |
|
|
214 |
|
|
215 |
|
|
216 |
|
|
217 |
|
public String[] searchUsersFromGroupByUser(final String groupPrincipalUid, SearchControls cons) |
218 |
|
throws NamingException |
219 |
|
{ |
220 |
|
|
221 |
0 |
String query = "(&(" + getUserGroupMembershipAttribute() + "=" + getGroupDN(groupPrincipalUid) + ")" + getUserFilter() + ")"; |
222 |
0 |
if (logger.isDebugEnabled()) |
223 |
|
{ |
224 |
0 |
logger.debug("query[" + query + "]"); |
225 |
|
} |
226 |
|
|
227 |
0 |
cons.setSearchScope(getSearchScope()); |
228 |
0 |
NamingEnumeration results = ((DirContext) ctx).search(getUserFilterBase(),query , cons); |
229 |
|
|
230 |
0 |
ArrayList userPrincipalUids = new ArrayList(); |
231 |
|
|
232 |
0 |
while (results.hasMore()) |
233 |
|
{ |
234 |
0 |
SearchResult result = (SearchResult) results.next(); |
235 |
0 |
Attributes answer = result.getAttributes(); |
236 |
0 |
userPrincipalUids.addAll(getAttributes(getAttribute(getUserIdAttribute(), answer))); |
237 |
0 |
} |
238 |
0 |
return (String[]) userPrincipalUids.toArray(new String[userPrincipalUids.size()]); |
239 |
|
} |
240 |
|
|
241 |
|
public String[] searchRolesFromGroupByGroup(final String groupPrincipalUid, |
242 |
|
SearchControls cons) throws NamingException { |
243 |
|
|
244 |
0 |
String query = "(&(" + getGroupIdAttribute() + "=" + (groupPrincipalUid) + ")" + getGroupFilter() + ")"; |
245 |
|
|
246 |
0 |
if (logger.isDebugEnabled()) { |
247 |
0 |
logger.debug("query[" + query + "]"); |
248 |
|
} |
249 |
|
|
250 |
0 |
ArrayList rolePrincipalUids = new ArrayList(); |
251 |
|
|
252 |
0 |
cons.setSearchScope(getSearchScope()); |
253 |
0 |
NamingEnumeration groups = ((DirContext) ctx).search(getGroupFilterBase(),query , cons); |
254 |
|
|
255 |
0 |
while (groups.hasMore()) { |
256 |
0 |
SearchResult group = (SearchResult) groups.next(); |
257 |
0 |
Attributes groupAttributes = group.getAttributes(); |
258 |
|
|
259 |
0 |
Attribute rolesFromGroup = getAttribute(getGroupMembershipForRoleAttribute(), groupAttributes); |
260 |
0 |
List roleDNs = getAttributes(rolesFromGroup,getRoleFilterBase()); |
261 |
0 |
Iterator it = roleDNs.iterator(); |
262 |
0 |
while (it.hasNext()) { |
263 |
0 |
String roleDN = (String) it.next(); |
264 |
0 |
if (!StringUtils.isEmpty(roleDN)) { |
265 |
0 |
String roleId = extractLdapAttr(roleDN,getRoleUidAttribute()); |
266 |
0 |
if (roleId!=null) { |
267 |
0 |
NamingEnumeration rolesResults = searchRoleByWildcardedUid(roleId, cons); |
268 |
0 |
if (rolesResults.hasMore()) |
269 |
0 |
if(rolesResults.nextElement()!=null) |
270 |
0 |
rolePrincipalUids.add(roleId); |
271 |
|
} |
272 |
|
} |
273 |
0 |
} |
274 |
0 |
} |
275 |
0 |
return (String[]) rolePrincipalUids.toArray(new String[rolePrincipalUids.size()]); |
276 |
|
} |
277 |
|
|
278 |
|
|
279 |
|
|
280 |
|
|
281 |
|
|
282 |
|
|
283 |
|
|
284 |
|
public String[] searchRolesFromGroupByRole(final String groupPrincipalUid, |
285 |
|
SearchControls cons) throws NamingException { |
286 |
|
|
287 |
0 |
String query = "(&(" + getRoleGroupMembershipForRoleAttribute() + "=" + getGroupDN(groupPrincipalUid) + ")" + getRoleFilter() + ")"; |
288 |
|
|
289 |
0 |
if (logger.isDebugEnabled()) { |
290 |
0 |
logger.debug("query[" + query + "]"); |
291 |
|
} |
292 |
|
|
293 |
0 |
cons.setSearchScope(getSearchScope()); |
294 |
0 |
NamingEnumeration results = ((DirContext) ctx).search(getRoleFilterBase(),query , cons); |
295 |
|
|
296 |
0 |
ArrayList rolePrincipalUids = new ArrayList(); |
297 |
|
|
298 |
0 |
while (results.hasMore()) { |
299 |
0 |
SearchResult result = (SearchResult) results.next(); |
300 |
0 |
Attributes answer = result.getAttributes(); |
301 |
0 |
rolePrincipalUids.addAll(getAttributes(getAttribute(getRoleIdAttribute(), answer))); |
302 |
0 |
} |
303 |
0 |
return (String[]) rolePrincipalUids |
304 |
|
.toArray(new String[rolePrincipalUids.size()]); |
305 |
|
} |
306 |
|
|
307 |
|
|
308 |
|
|
309 |
|
|
310 |
|
|
311 |
|
public String[] searchUsersFromRoleByRole(final String rolePrincipalUid, SearchControls cons) |
312 |
|
throws NamingException |
313 |
|
{ |
314 |
|
|
315 |
0 |
String query = "(&(" + getRoleIdAttribute() + "=" + (rolePrincipalUid) + ")" + getRoleFilter() + ")"; |
316 |
|
|
317 |
0 |
if (logger.isDebugEnabled()) |
318 |
|
{ |
319 |
0 |
logger.debug("query[" + query + "]"); |
320 |
|
} |
321 |
|
|
322 |
0 |
ArrayList userPrincipalUids=new ArrayList(); |
323 |
|
|
324 |
0 |
cons.setSearchScope(getSearchScope()); |
325 |
0 |
NamingEnumeration results = ((DirContext) ctx).search(getRoleFilterBase(),query , cons); |
326 |
|
|
327 |
0 |
while (results.hasMore()) |
328 |
|
{ |
329 |
0 |
SearchResult result = (SearchResult) results.next(); |
330 |
0 |
Attributes answer = result.getAttributes(); |
331 |
|
|
332 |
0 |
Attribute userPrincipalUid = getAttribute(getRoleMembershipAttribute(), answer); |
333 |
0 |
List attrs = getAttributes(userPrincipalUid); |
334 |
0 |
Iterator it = attrs.iterator(); |
335 |
0 |
while(it.hasNext()) { |
336 |
0 |
String uidfull = (String)it.next(); |
337 |
0 |
if (!StringUtils.isEmpty(uidfull)) { |
338 |
0 |
String uid = extractLdapAttr(uidfull,getUserIdAttribute()); |
339 |
0 |
if (uid!=null){ |
340 |
0 |
userPrincipalUids.add(uid); |
341 |
|
} |
342 |
|
} |
343 |
0 |
} |
344 |
0 |
} |
345 |
0 |
return (String[]) userPrincipalUids.toArray(new String[userPrincipalUids.size()]); |
346 |
|
} |
347 |
|
|
348 |
|
|
349 |
|
|
350 |
|
|
351 |
|
public String[] searchUsersFromRoleByUser(final String rolePrincipalUid, SearchControls cons) |
352 |
|
throws NamingException |
353 |
|
{ |
354 |
0 |
String roleMemberAttr = getUserRoleMembershipAttribute(); |
355 |
|
|
356 |
|
|
357 |
|
|
358 |
|
|
359 |
|
|
360 |
0 |
StringBuffer byRolePrincipalUidMatch = new StringBuffer("(").append(roleMemberAttr).append("=").append(rolePrincipalUid).append(")"); |
361 |
0 |
StringBuffer byRoleDNMatch = new StringBuffer("(").append(roleMemberAttr).append("=").append(getRoleDN(rolePrincipalUid, true)).append(")"); |
362 |
|
|
363 |
0 |
StringBuffer completeRoleAttrMatch = new StringBuffer("(|").append(byRolePrincipalUidMatch).append(byRoleDNMatch).append(")"); |
364 |
0 |
StringBuffer query= new StringBuffer("(&").append(completeRoleAttrMatch).append("(").append(getUserFilter()).append("))"); |
365 |
|
|
366 |
0 |
if (logger.isDebugEnabled()) |
367 |
|
{ |
368 |
0 |
logger.debug("query[" + query + "]"); |
369 |
|
} |
370 |
|
|
371 |
0 |
cons.setSearchScope(getSearchScope()); |
372 |
0 |
NamingEnumeration results = ((DirContext) ctx).search(getUserFilterBase(),query.toString() , cons); |
373 |
|
|
374 |
0 |
ArrayList userPrincipalUids = new ArrayList(); |
375 |
|
|
376 |
0 |
while (results.hasMore()) |
377 |
|
{ |
378 |
0 |
SearchResult result = (SearchResult) results.next(); |
379 |
0 |
Attributes answer = result.getAttributes(); |
380 |
0 |
userPrincipalUids.addAll(getAttributes(getAttribute(getUserIdAttribute(), answer))); |
381 |
0 |
} |
382 |
0 |
return (String[]) userPrincipalUids.toArray(new String[userPrincipalUids.size()]); |
383 |
|
} |
384 |
|
|
385 |
|
|
386 |
|
|
387 |
|
|
388 |
|
|
389 |
|
|
390 |
|
protected List getAttributes(Attribute attr) throws NamingException |
391 |
|
{ |
392 |
0 |
return getAttributes(attr, null); |
393 |
|
} |
394 |
|
|
395 |
|
|
396 |
|
|
397 |
|
|
398 |
|
|
399 |
|
protected List getAttributes(Attribute attr,String filter) throws NamingException |
400 |
|
{ |
401 |
0 |
List uids = new ArrayList(); |
402 |
0 |
if (attr != null) |
403 |
|
{ |
404 |
0 |
Enumeration groupUidEnum = attr.getAll(); |
405 |
0 |
while (groupUidEnum.hasMoreElements()) |
406 |
|
{ |
407 |
0 |
String groupDN = (String)groupUidEnum.nextElement(); |
408 |
0 |
if (filter==null) { |
409 |
0 |
uids.add(groupDN); |
410 |
0 |
} else if (filter!=null && groupDN.toLowerCase().indexOf(filter.toLowerCase())!=-1) { |
411 |
0 |
uids.add(groupDN); |
412 |
|
} |
413 |
0 |
} |
414 |
|
} |
415 |
0 |
return uids; |
416 |
|
} |
417 |
|
|
418 |
|
|
419 |
|
|
420 |
|
|
421 |
|
|
422 |
|
|
423 |
|
private Attributes getFirstUser(NamingEnumeration results) throws NamingException |
424 |
|
{ |
425 |
0 |
SearchResult result = (SearchResult) results.next(); |
426 |
0 |
Attributes answer = result.getAttributes(); |
427 |
|
|
428 |
0 |
return answer; |
429 |
|
} |
430 |
|
|
431 |
|
|
432 |
|
|
433 |
|
|
434 |
|
|
435 |
|
|
436 |
|
|
437 |
|
|
438 |
|
|
439 |
|
|
440 |
|
|
441 |
|
protected Attributes defineLdapAttributes(final String principalUid) |
442 |
|
{ |
443 |
0 |
Attributes attrs = new BasicAttributes(true); |
444 |
0 |
BasicAttribute classes = new BasicAttribute("objectclass"); |
445 |
|
|
446 |
0 |
classes.add("top"); |
447 |
0 |
classes.add("person"); |
448 |
0 |
classes.add("organizationalPerson"); |
449 |
0 |
classes.add("inetorgperson"); |
450 |
0 |
attrs.put(classes); |
451 |
0 |
attrs.put("cn", principalUid); |
452 |
0 |
attrs.put("sn", principalUid); |
453 |
|
|
454 |
0 |
return attrs; |
455 |
|
} |
456 |
|
|
457 |
|
|
458 |
|
|
459 |
|
|
460 |
|
protected String getDnSuffix() |
461 |
|
{ |
462 |
0 |
return this.getUserFilterBase(); |
463 |
|
} |
464 |
|
|
465 |
|
|
466 |
|
|
467 |
|
|
468 |
|
|
469 |
|
|
470 |
|
|
471 |
|
|
472 |
|
|
473 |
|
protected Principal makePrincipal(String principalUid) |
474 |
|
{ |
475 |
0 |
return new UserPrincipalImpl(principalUid); |
476 |
|
} |
477 |
|
|
478 |
|
private String extractLdapAttr(String dn,String ldapAttrName) { |
479 |
|
|
480 |
0 |
String dnLowerCase = dn.toLowerCase(); |
481 |
0 |
String ldapAttrNameLowerCase = ldapAttrName.toLowerCase(); |
482 |
|
|
483 |
0 |
if (dnLowerCase.indexOf(ldapAttrNameLowerCase + "=")==-1) |
484 |
0 |
return null; |
485 |
|
|
486 |
0 |
if (dn.indexOf(",")!=-1 && dnLowerCase.indexOf(ldapAttrNameLowerCase + "=")!=-1) |
487 |
0 |
return dn.substring(dnLowerCase.indexOf(ldapAttrNameLowerCase)+ldapAttrName.length()+1,dn.indexOf(",")); |
488 |
0 |
return dn.substring(dnLowerCase.indexOf(ldapAttrNameLowerCase)+ldapAttrName.length()+1,dn.length()); |
489 |
|
} |
490 |
|
|
491 |
|
protected String[] getObjectClasses() { |
492 |
0 |
return this.getUserObjectClasses(); |
493 |
|
} |
494 |
|
|
495 |
|
protected String getUidAttributeForPrincipal() { |
496 |
0 |
return this.getUserUidAttribute(); |
497 |
|
} |
498 |
|
|
499 |
|
protected String[] getAttributes() { |
500 |
0 |
return getUserAttributes(); |
501 |
|
} |
502 |
|
|
503 |
|
protected String getEntryPrefix() |
504 |
|
{ |
505 |
0 |
return this.getUidAttribute(); |
506 |
|
} |
507 |
|
|
508 |
|
protected String getSearchSuffix() { |
509 |
0 |
return this.getUserFilter(); |
510 |
|
} |
511 |
|
} |