001: /***********************************************************************************
002: *
003: * Copyright (c) 2003, 2004, 2005, 2006 The Sakai Foundation.
004: *
005: * Licensed under the Educational Community License, Version 1.0 (the "License");
006: * you may not use this file except in compliance with the License.
007: * You may obtain a copy of the License at
008: *
009: * http://www.opensource.org/licenses/ecl1.php
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: **********************************************************************************/package org.sakaiproject.archive.impl;
018:
019: import java.io.File;
020: import java.util.HashMap;
021: import java.util.HashSet;
022: import java.util.List;
023: import java.util.Map;
024: import java.util.Vector;
025:
026: import org.apache.commons.codec.binary.Base64;
027: import org.apache.commons.logging.Log;
028: import org.apache.commons.logging.LogFactory;
029: import org.sakaiproject.authz.api.AuthzGroup;
030: import org.sakaiproject.authz.api.Role;
031: import org.sakaiproject.authz.api.AuthzGroupService;
032: import org.sakaiproject.authz.api.SecurityService;
033: import org.sakaiproject.component.cover.ComponentManager;
034: import org.sakaiproject.content.api.ContentHostingService;
035: import org.sakaiproject.entity.api.EntityProducer;
036: import org.sakaiproject.exception.IdInvalidException;
037: import org.sakaiproject.exception.IdUnusedException;
038: import org.sakaiproject.exception.IdUsedException;
039: import org.sakaiproject.exception.InUseException;
040: import org.sakaiproject.exception.PermissionException;
041: import org.sakaiproject.site.api.Site;
042: import org.sakaiproject.site.api.SiteService;
043: import org.sakaiproject.user.api.User;
044: import org.sakaiproject.user.api.UserAlreadyDefinedException;
045: import org.sakaiproject.user.api.UserEdit;
046: import org.sakaiproject.user.api.UserIdInvalidException;
047: import org.sakaiproject.user.api.UserNotDefinedException;
048: import org.sakaiproject.user.api.UserPermissionException;
049: import org.sakaiproject.user.api.UserDirectoryService;
050: import org.sakaiproject.util.Xml;
051: import org.w3c.dom.Document;
052: import org.w3c.dom.Element;
053: import org.w3c.dom.Node;
054: import org.w3c.dom.NodeList;
055: import org.sakaiproject.archive.api.ArchiveService;
056:
057: public class SiteMerger {
058: private static Log M_log = LogFactory.getLog(SiteMerger.class);
059:
060: protected static HashMap userIdTrans = new HashMap();
061:
062: /**********************************************/
063: /* Injected Dependencies */
064: /**********************************************/
065: protected AuthzGroupService m_authzGroupService = null;
066:
067: public void setAuthzGroupService(AuthzGroupService service) {
068: m_authzGroupService = service;
069: }
070:
071: protected UserDirectoryService m_userDirectoryService = null;
072:
073: public void setUserDirectoryService(UserDirectoryService service) {
074: m_userDirectoryService = service;
075: }
076:
077: protected SiteService m_siteService = null;
078:
079: public void setSiteService(SiteService service) {
080: m_siteService = service;
081: }
082:
083: protected SecurityService m_securityService = null;
084:
085: public void setSecurityService(SecurityService service) {
086: m_securityService = service;
087: }
088:
089: // only the resources created by the followinng roles will be imported
090: // role sets are different to different system
091: //public String[] SAKAI_roles = m_filteredSakaiRoles; //= {"Affiliate", "Assistant", "Instructor", "Maintain", "Organizer", "Owner"};
092:
093: // tool id updates
094: private String old_toolId_prefix = "chef.";
095: private String new_toolId_prefix = "sakai.";
096: private String[] old_toolIds = { "sakai.noti.prefs",
097: "sakai.presence", "sakai.siteinfogeneric",
098: "sakai.sitesetupgeneric", "sakai.threadeddiscussion" };
099: private String[] new_toolIds = { "sakai.preferences",
100: "sakai.online", "sakai.siteinfo", "sakai.sitesetup",
101: "sakai.discussion" };
102:
103: //SWG TODO I have a feeling this is a bug
104: protected HashSet UsersListAllowImport = new HashSet();
105:
106: /**
107: * Process a merge for the file, or if it's a directory, for all contained files (one level deep).
108: * @param fileName The site name (for the archive file) to read from.
109: * @param mergeId The id string to use to make ids in the merge consistent and unique.
110: * @param creatorId The creator id
111: * If null or blank, the date/time string of the merge is used.
112: */
113: //TODO Javadoc this
114: public String merge(String fileName, String siteId,
115: String creatorId, String m_storagePath,
116: boolean filterSakaiServices,
117: String[] filteredSakaiServices, boolean filterSakaiRoles,
118: String[] filteredSakaiRoles) {
119: StringBuffer results = new StringBuffer();
120:
121: File[] files = null;
122:
123: // see if the name is a directory
124: File file = new File(m_storagePath + fileName);
125: if ((file == null) || (!file.exists())) {
126: results.append("file: " + file.getPath() + " not found.\n");
127: M_log.warn("merge(): file not found: " + file.getPath());
128: return results.toString();
129: }
130:
131: if (file.isDirectory()) {
132: files = file.listFiles();
133: } else {
134: files = new File[1];
135: files[0] = file;
136: }
137:
138: // track old to new attachment names
139: Map attachmentNames = new HashMap();
140:
141: // firstly, merge the users
142: for (int i = 0; i < files.length; i++) {
143: if ((files[i] != null)
144: && (files[i].getPath().indexOf("user.xml") != -1)) {
145: processMerge(files[i].getPath(), siteId, results,
146: attachmentNames, null, filterSakaiServices,
147: filteredSakaiServices, filterSakaiRoles,
148: filteredSakaiRoles);
149: files[i] = null;
150: break;
151: }
152: }
153:
154: // see if there's a site definition
155: for (int i = 0; i < files.length; i++) {
156: if ((files[i] != null)
157: && (files[i].getPath().indexOf("site.xml") != -1)) {
158: processMerge(files[i].getPath(), siteId, results,
159: attachmentNames, creatorId,
160: filterSakaiServices, filteredSakaiServices,
161: filterSakaiRoles, filteredSakaiRoles);
162: files[i] = null;
163: break;
164: }
165: }
166:
167: // see if there's an attachments definition
168: for (int i = 0; i < files.length; i++) {
169: if ((files[i] != null)
170: && (files[i].getPath().indexOf("attachment.xml") != -1)) {
171: processMerge(files[i].getPath(), siteId, results,
172: attachmentNames, null, filterSakaiServices,
173: filteredSakaiServices, filterSakaiRoles,
174: filteredSakaiRoles);
175: files[i] = null;
176: break;
177: }
178: }
179:
180: // process each remaining file that is an .xml file
181: for (int i = 0; i < files.length; i++) {
182: if (files[i] != null)
183: if (files[i].getPath().endsWith(".xml"))
184: processMerge(files[i].getPath(), siteId, results,
185: attachmentNames, null, filterSakaiServices,
186: filteredSakaiServices, filterSakaiRoles,
187: filteredSakaiRoles);
188: }
189:
190: return results.toString();
191:
192: } // merge
193:
194: /**
195: * Read in an archive file and merge the entries into the specified site.
196: * @param fileName The site name (for the archive file) to read from.
197: * @param siteId The id of the site to merge the content into.
198: * @param results A buffer to accumulate result messages.
199: * @param attachmentNames A map of old to new attachment names.
200: * @param useIdTrans A map of old WorkTools id to new Ctools id
201: * @param creatorId The creator id
202: */
203: protected void processMerge(String fileName, String siteId,
204: StringBuffer results, Map attachmentNames,
205: String creatorId, boolean filterSakaiService,
206: String[] filteredSakaiService, boolean filterSakaiRoles,
207: String[] filteredSakaiRoles) {
208: // correct for windows backslashes
209: fileName = fileName.replace('\\', '/');
210:
211: if (M_log.isDebugEnabled())
212: M_log.debug("merge(): processing file: " + fileName);
213:
214: Site theSite = null;
215: try {
216: theSite = m_siteService.getSite(siteId);
217: } catch (IdUnusedException ignore) {
218: M_log.warn(ignore, ignore);
219: }
220:
221: // read the whole file into a DOM
222: Document doc = Xml.readDocument(fileName);
223: if (doc == null) {
224: results
225: .append("Error reading xml from: " + fileName
226: + "\n");
227: return;
228: }
229:
230: // verify the root element
231: Element root = doc.getDocumentElement();
232: if (!root.getTagName().equals("archive")) {
233: results
234: .append("File: "
235: + fileName
236: + " does not contain archive xml. Found this root tag: "
237: + root.getTagName() + "\n");
238: return;
239: }
240:
241: // get the from site id
242: String fromSite = root.getAttribute("source");
243: String system = root.getAttribute("system");
244:
245: // the children
246: NodeList children = root.getChildNodes();
247: final int length = children.getLength();
248: for (int i = 0; i < length; i++) {
249: Node child = children.item(i);
250: if (child.getNodeType() != Node.ELEMENT_NODE)
251: continue;
252: Element element = (Element) child;
253:
254: // look for site stuff
255: if (element.getTagName().equals(SiteService.APPLICATION_ID)) {
256: //if the xml file is from WT site, merge it with the translated user ids
257: //if (system.equalsIgnoreCase(ArchiveService.FROM_WT))
258: // mergeSite(siteId, fromSite, element, userIdTrans, creatorId);
259: //else
260: mergeSite(siteId, fromSite, element,
261: new HashMap()/*empty userIdMap */, creatorId,
262: filterSakaiRoles, filteredSakaiRoles);
263: } else if (element.getTagName().equals(
264: UserDirectoryService.APPLICATION_ID)) {
265: ;
266: // Apparently, users have only been merged in they are from WorkTools...
267: // Is this every going to be wanted in Sakai?
268: // String msg = mergeUsers(element, userIdTrans);
269: // results.append(msg);
270: }
271:
272: else {
273: // we need a site now
274: if (theSite == null) {
275: results.append("Site: " + siteId + " not found.\n");
276: return;
277: }
278:
279: // get the service name
280: String serviceName = translateServiceName(element
281: .getTagName());
282:
283: // get the service
284: try {
285: EntityProducer service = (EntityProducer) ComponentManager
286: .get(serviceName);
287:
288: try {
289: String msg = "";
290: if (system
291: .equalsIgnoreCase(ArchiveService.FROM_SAKAI)
292: && (checkSakaiService(
293: filterSakaiService,
294: filteredSakaiService,
295: serviceName)))
296: msg = service
297: .merge(
298: siteId,
299: element,
300: fileName,
301: fromSite,
302: attachmentNames,
303: new HashMap() /* empty userIdTran map */,
304: UsersListAllowImport);
305:
306: results.append(msg);
307: } catch (Throwable t) {
308: results.append("Error merging: " + serviceName
309: + " in file: " + fileName + " : "
310: + t.toString() + "\n");
311: M_log.warn("Error merging: " + serviceName
312: + " in file: " + fileName + " : "
313: + t.toString(), t);
314: }
315: } catch (Throwable t) {
316: results
317: .append("Did not recognize the resource service: "
318: + serviceName
319: + " in file: "
320: + fileName + "\n");
321: M_log.warn(
322: "Did not recognize the resource service: "
323: + serviceName + " in file: "
324: + fileName, t);
325: }
326: }
327: }
328:
329: } // processMerge
330:
331: /**
332: * Merge the site definition from the site part of the archive file into the site service.
333: * Translate the id to the siteId.
334: * @param siteId The id of the site getting imported into.
335: * @param fromSiteId The id of the site the archive was made from.
336: * @param element The XML DOM tree of messages to merge.
337: * @param creatorId The creator id
338: */
339: protected void mergeSite(String siteId, String fromSiteId,
340: Element element, HashMap useIdTrans, String creatorId,
341: boolean filterSakaiRoles, String[] filteredSakaiRoles) {
342: String source = "";
343:
344: Node parent = element.getParentNode();
345: if (parent.getNodeType() == Node.ELEMENT_NODE) {
346: Element parentEl = (Element) parent;
347: source = parentEl.getAttribute("system");
348: }
349:
350: NodeList children = element.getChildNodes();
351: final int length = children.getLength();
352: for (int i = 0; i < length; i++) {
353: Node child = children.item(i);
354: if (child.getNodeType() != Node.ELEMENT_NODE)
355: continue;
356: Element element2 = (Element) child;
357: if (!element2.getTagName().equals("site"))
358: continue;
359:
360: NodeList toolChildren = element2
361: .getElementsByTagName("tool");
362: final int tLength = toolChildren.getLength();
363: for (int i2 = 0; i2 < tLength; i2++) {
364: Element element3 = (Element) toolChildren.item(i2);
365: String toolId = element3.getAttribute("toolId");
366: if (toolId != null) {
367: toolId = toolId.replaceAll(old_toolId_prefix,
368: new_toolId_prefix);
369: for (int j = 0; j < old_toolIds.length; j++) {
370: toolId = toolId.replaceAll(old_toolIds[i],
371: new_toolIds[i]);
372: }
373: }
374: element3.setAttribute("toolId", toolId);
375: }
376:
377: // merge the site info first
378: try {
379: m_siteService.merge(siteId, element2, creatorId);
380: mergeSiteInfo(element2, siteId);
381: } catch (Exception any) {
382: M_log.warn(any, any);
383: }
384:
385: Site site = null;
386: try {
387: site = m_siteService.getSite(siteId);
388: } catch (IdUnusedException e) {
389: M_log.warn(this + "The site with id " + siteId
390: + " doesn't exit", e);
391: return;
392: }
393:
394: if (site != null) {
395: NodeList children2 = element2.getChildNodes();
396: final int length2 = children2.getLength();
397: for (int i2 = 0; i2 < length2; i2++) {
398: Node child2 = children2.item(i2);
399: if (child2.getNodeType() != Node.ELEMENT_NODE)
400: continue;
401: Element element3 = (Element) child2;
402: if (!element3.getTagName().equals("roles"))
403: continue;
404:
405: try {
406: mergeSiteRoles(element3, siteId, useIdTrans,
407: filterSakaiRoles, filteredSakaiRoles);
408: } catch (PermissionException e1) {
409: M_log.warn(e1, e1);
410: }
411: }
412: }
413: }
414: } // mergeSite
415:
416: /**
417: * Merge the site info like description from the site part of the archive file into the site service.
418: * @param element The XML DOM tree of messages to merge.
419: * @param siteId The id of the site getting imported into.
420: */
421: protected void mergeSiteInfo(Element el, String siteId)
422: throws IdInvalidException, IdUsedException,
423: PermissionException, IdUnusedException, InUseException {
424: // heck security (throws if not permitted)
425: unlock(SiteService.SECURE_UPDATE_SITE, m_siteService
426: .siteReference(siteId));
427:
428: Site edit = m_siteService.getSite(siteId);
429: String desc = el.getAttribute("description-enc");
430:
431: try {
432: byte[] decoded = Base64
433: .decodeBase64(desc.getBytes("UTF-8"));
434: byte[] filteredDecoded = decoded;
435: for (int i = 0; i < decoded.length; i++) {
436: byte b = decoded[i];
437: if (b == (byte) -109 || b == (byte) -108) {
438: // smart quotes, open/close double quote
439: filteredDecoded[i] = (byte) 34;
440: } else if (b == (byte) -111 || b == (byte) -110) {
441: // smart quotes, open/close double quote
442: filteredDecoded[i] = (byte) 39;
443: } else if (b == (byte) -106) {
444: // dash
445: filteredDecoded[i] = (byte) 45;
446: }
447: }
448: desc = new String(decoded, "UTF-8");
449: } catch (Exception any) {
450: M_log.warn("mergeSiteInfo(): exception caught", any);
451: }
452: //edit.setTitle(title);
453: edit.setDescription(desc);
454:
455: m_siteService.save(edit);
456:
457: return;
458:
459: } // mergeSiteInfo
460:
461: /**
462: * Merge the the permission-roles settings into the site
463: * @param element The XML DOM tree of messages to merge.
464: * @param siteId The id of the site getting imported into.
465: */
466: protected void mergeSiteRoles(Element el, String siteId,
467: HashMap useIdTrans, boolean filterSakaiRoles,
468: String[] filteredSakaiRoles) throws PermissionException {
469: // heck security (throws if not permitted)
470: unlock(SiteService.SECURE_UPDATE_SITE, m_siteService
471: .siteReference(siteId));
472:
473: String source = "";
474:
475: // el: <roles> node
476: Node parent0 = el.getParentNode(); // parent0: <site> node
477: Node parent1 = parent0.getParentNode(); // parent1: <service> node
478: Node parent = parent1.getParentNode(); // parent: <archive> node containing "system"
479:
480: if (parent.getNodeType() == Node.ELEMENT_NODE) {
481: Element parentEl = (Element) parent;
482: source = parentEl.getAttribute("system");
483: }
484:
485: List roles = new Vector();
486: //List maintainUsers = new Vector();
487: //List accessUsers = new Vector();
488:
489: // to add this user with this role inito this realm
490: String realmId = m_siteService.siteReference(siteId); //SWG "/site/" + siteId;
491: try {
492: AuthzGroup realm = m_authzGroupService
493: .getAuthzGroup(realmId);
494: roles.addAll(realm.getRoles());
495:
496: NodeList children = el.getChildNodes();
497: final int length = children.getLength();
498: for (int i = 0; i < length; i++) {
499: Node child = children.item(i);
500: if (child.getNodeType() != Node.ELEMENT_NODE)
501: continue;
502: Element element2 = (Element) child;
503:
504: //SWG Getting rid of WT part above, this was previously the else branch labeled "for both CT classic and Sakai CTools"
505: // check is this roleId is a qualified one
506: if (!checkSystemRole(source, element2.getTagName(),
507: filterSakaiRoles, filteredSakaiRoles))
508: continue;
509:
510: NodeList children2 = element2.getChildNodes();
511: final int length2 = children2.getLength();
512: for (int i2 = 0; i2 < length2; i2++) {
513: Node child2 = children2.item(i2);
514: if (child2.getNodeType() != Node.ELEMENT_NODE)
515: continue;
516: Element element3 = (Element) child2;
517: if (!element3.getTagName().equals("ability"))
518: continue;
519:
520: String userId = element3.getAttribute("userId");
521: // this user has a qualified role, his/her resource will be imported
522: UsersListAllowImport.add(userId);
523: }
524: } // for
525: } catch (Exception err) {
526: M_log.warn("()mergeSiteRoles realm edit exception caught"
527: + realmId, err);
528: }
529: return;
530:
531: } // mergeSiteRoles
532:
533: /**
534: * Merge the user list into the the system.
535: * Translate the id to the siteId.
536: * @param element The XML DOM tree of messages to merge.
537: */
538: //SWG This seems to have been abandoned for anything for WorkTools.
539: // If we need the ability to import users again, see ArchiveServiceImpl.java
540: // for the implementation of this method.
541: //protected String mergeUsers(Element element, HashMap useIdTrans)
542: //throws IdInvalidException, IdUsedException, PermissionException
543:
544: /**
545: * Old archives have the old CHEF 1.2 service names...
546: */
547: protected String translateServiceName(String name) {
548: if ("org.chefproject.service.GenericContentHostingService"
549: .equals(name)) {
550: return ContentHostingService.class.getName();
551: }
552:
553: return name;
554: }
555:
556: /*
557: *
558: */
559: protected boolean checkSakaiService(boolean m_filterSakaiServices,
560: String[] m_filteredSakaiServices, String serviceName) {
561: if (m_filterSakaiServices) {
562: for (int i = 0; i < m_filteredSakaiServices.length; i++) {
563: if (serviceName.endsWith(m_filteredSakaiServices[i]
564: .toString())) {
565: return true;
566: }
567: }
568: return false;
569: } else {
570: return true;
571: }
572: }
573:
574: /**
575: * Check security permission.
576: * @param lock The lock id string.
577: * @param reference The resource's reference string, or null if no resource is involved.
578: * @exception PermissionException thrown if the user does not have access
579: */
580: protected void unlock(String lock, String reference)
581: throws PermissionException {
582: if (!m_securityService.unlock(lock, reference)) {
583: // needs to bring back: where is sessionService
584: // throw new PermissionException(UsageSessionService.getSessionUserId(), lock, reference);
585: }
586: } // unlock
587:
588: /**
589: * When Sakai is importing a role in site.xml, check if it is a qualified role.
590: * @param roleId
591: * @return boolean value - true: the role is accepted for importing; otherwise, not;
592: */
593: protected boolean checkSystemRole(String system, String roleId,
594: boolean filterSakaiRoles, String[] filteredSakaiRoles) {
595: if (system.equalsIgnoreCase(ArchiveService.FROM_SAKAI)) {
596: if (filterSakaiRoles) {
597: for (int i = 0; i < filteredSakaiRoles.length; i++) {
598: if (!filteredSakaiRoles[i].equalsIgnoreCase(roleId))
599: return true;
600: }
601: } else {
602: return true;
603: }
604: }
605: return false;
606: }
607: }
|