1 //
  2 // The Antville Project
  3 // http://code.google.com/p/antville
  4 //
  5 // Copyright 2001-2007 by The Antville People
  6 //
  7 // Licensed under the Apache License, Version 2.0 (the ``License'');
  8 // you may not use this file except in compliance with the License.
  9 // You may obtain a copy of the License at
 10 //
 11 //    http://www.apache.org/licenses/LICENSE-2.0
 12 //
 13 // Unless required by applicable law or agreed to in writing, software
 14 // distributed under the License is distributed on an ``AS IS'' BASIS,
 15 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 16 // See the License for the specific language governing permissions and
 17 // limitations under the License.
 18 //
 19 // $Revision$
 20 // $LastChangedBy$
 21 // $LastChangedDate$
 22 // $URL$
 23 //
 24 
 25 /**
 26  * @fileOverview Defines the Layout prototype
 27  */
 28 
 29 markgettext("Layout");
 30 markgettext("layout");
 31 
 32 /** @constant */
 33 Layout.VALUES = [
 34    "background color",
 35    "link color",
 36    "active link color",
 37    "visited link color",
 38    "big font",
 39    "big font size",
 40    "big font color",
 41    "base font",
 42    "base font size",
 43    "base font color",
 44    "small font",
 45    "small font size",
 46    "small font color"
 47 ];
 48 
 49 /**
 50  * 
 51  * @param {Layout} layout
 52  * @param {Boolean} includeSelf
 53  */
 54 Layout.remove = function(options) {
 55    if (this.constructor === Layout) {
 56       // Backup current layout in temporary directory if possible
 57       var dir = this.getFile();
 58       if (res.skinpath && res.skinpath[0].equals(dir) && 
 59                dir.exists() && dir.list().length > 0) {
 60          var zip = this.getArchive(res.skinpath);
 61          var file = java.io.File.createTempFile(this.site.name + "-layout-", ".zip");
 62          zip.save(file);
 63       }
 64       HopObject.remove.call(this.skins);
 65       HopObject.remove.call(this.images);
 66       this.getFile().removeDirectory();
 67       // The “force” flag is set e.g. when a whole site is removed
 68       options && options.force && this.remove();
 69    }
 70    return;
 71 }
 72 
 73 /** 
 74  * @function
 75  * @returns {String[]}
 76  * @see defineConstants
 77  */
 78 Layout.getModes = defineConstants(Layout, "default", "shared");
 79 
 80 this.handleMetadata("origin");
 81 this.handleMetadata("originator");
 82 this.handleMetadata("originated");
 83 
 84 /**
 85  * @name Layout
 86  * @constructor
 87  * @param {Site} site
 88  * @property {Date} created
 89  * @property {User} creator
 90  * @property {Images} images
 91  * @property {Metadata} metadata
 92  * @property {String} mode
 93  * @property {Date} modified
 94  * @property {User} modifier
 95  * @property {String} origin
 96  * @property {String} originator
 97  * @property {Date} originated
 98  * @property {Site} site
 99  * @property {Skins} skins
100  * @extends HopObject
101  */
102 Layout.prototype.constructor = function(site) {
103    this.site = site;
104    this.creator = session.user;
105    this.created = new Date;
106    this.mode = Layout.DEFAULT;
107    this.touch();
108    return this;
109 }
110 
111 /**
112  * 
113  * @param {String} action
114  * @returns {Boolean}
115  */
116 Layout.prototype.getPermission = function(action) {
117    switch (action) {
118       case ".":
119       case "main":
120       case "export":
121       case "images":
122       case "import":
123       case "reset":
124       case "skins":
125       return res.handlers.site.getPermission("main") &&
126             Membership.require(Membership.OWNER) ||
127             User.require(User.PRIVILEGED);
128    }
129    return false;
130 }
131 
132 // FIXME: The Layout.href method is overwritten to guarantee that
133 // URLs won't contain the layout ID instead of "layout"
134 /**
135  * 
136  * @param {String} action
137  * @returns {String}
138  */
139 Layout.prototype.href = function(action) {
140    res.push();
141    res.write(res.handlers.site.href());
142    res.write("layout/");
143    action && res.write(action);
144    return res.pop();
145 }
146 
147 Layout.prototype.main_action = function() {
148    if (req.postParams.save) {
149       try {
150          this.update(req.postParams);
151          res.message = gettext("Successfully updated the layout.");
152          res.redirect(this.href());
153       } catch (ex) {
154          res.message = ex;
155          app.log(ex);
156       }
157    }
158    res.data.title = gettext("Layout");
159    res.data.body = this.renderSkinAsString("$Layout#main");
160    res.handlers.site.renderSkin("Site#page");
161    return;
162 }
163 
164 /**
165  * 
166  * @param {String} name
167  * @returns {Object}
168  */
169 Layout.prototype.getFormOptions = function(name) {
170    switch (name) {
171       case "mode":
172       return Layout.getModes();
173       case "parent":
174       return this.getParentOptions();
175    }
176 }
177 
178 /**
179  * 
180  * @param {Object} data
181  */
182 Layout.prototype.update = function(data) {
183    var skin = this.skins.getSkin("Site", "values");
184    if (!skin) {
185       skin = new Skin("Site", "values");
186       this.skins.add(skin);
187    }
188    res.push();
189    for (var key in data) {
190       if (key.startsWith("value_")) {
191          var value = data[key];
192          key = key.substr(6);
193          res.write("<% value ");
194          res.write(quote(key));
195          res.write(" ");
196          res.write(quote(value));
197          res.write(" %>\n");
198       }
199    }
200    res.write("\n");
201    skin.setSource(res.pop());
202    this.description = data.description;
203    this.mode = data.mode;
204    this.touch();
205    return;
206 }
207 
208 Layout.prototype.reset_action = function() {
209    if (req.data.proceed) {
210       try {
211          Layout.remove.call(this);
212          this.reset();
213          res.message = gettext("{0} was successfully reset.", gettext("Layout"));
214          res.redirect(this.href());
215       } catch(ex) {
216          res.message = ex;
217          app.log(ex);
218       }
219    }
220 
221    res.data.action = this.href(req.action);
222    res.data.title = gettext("Confirm Reset");
223    res.data.body = this.renderSkinAsString("$HopObject#confirm", {
224       text: this.getConfirmText()
225    });
226    res.handlers.site.renderSkin("Site#page");
227 }
228 
229 Layout.prototype.export_action = function() {
230    res.contentType = "application/zip";
231    var zip = this.getArchive(res.skinpath);
232    res.setHeader("Content-Disposition",
233          "attachment; filename=" + this.site.name + "-layout.zip");
234    res.writeBinary(zip.getData());
235    return;
236 }
237 
238 Layout.prototype.import_action = function() {
239    var self = this;
240    var data = req.postParams;
241    if (data.submit) {
242       try {
243          if (!data.upload || data.upload.contentLength === 0) {
244             throw Error(gettext("Please upload a zipped layout archive"));
245          }
246          // Extract zipped layout to temporary directory
247          var dir = this.site.getStaticFile();
248          var temp = new helma.File(dir, "import.temp");
249          var fname = data.upload.writeToFile(dir);
250          var zip = new helma.File(dir, fname);
251          (new helma.Zip(zip)).extractAll(temp);
252          zip.remove();
253          var data = Xml.read(new helma.File(temp, "data.xml"));
254          if (!data.version || data.version !== Root.VERSION) {
255             throw Error(gettext("Sorry, this layout is not compatible with Antville."));
256          }
257          // Remove current layout and replace it with imported one
258          Layout.remove.call(this);
259          temp.renameTo(this.getFile());
260          this.origin = data.origin;
261          this.originator = data.originator;
262          this.originated = data.originated;
263          data.images.forEach(function() {
264             self.images.add(new Image(this));
265          });
266          this.touch();
267          res.message = gettext("The layout was successfully imported.");
268       } catch (ex) {
269          res.message = ex;
270          app.log(ex);
271          temp && temp.removeDirectory();
272          res.redirect(this.href(req.action));
273       }
274       res.redirect(this.href());
275       return;
276    }
277    res.data.title = gettext("Import Layout");
278    res.data.body = this.renderSkinAsString("$Layout#import");
279    res.handlers.site.renderSkin("Site#page");
280    return;
281 }
282 
283 /**
284  * 
285  * @param {String} name
286  * @param {String} fallback
287  * @returns {Image}
288  */
289 Layout.prototype.getImage = function(name, fallback) {
290    var layout = this;
291    while (layout) {
292       if (layout.images.get(name)) {
293          return layout.images.get(name);
294       }
295       if (fallback && layout.images.get(fallback)) {
296          return layout.images.get(fallback);
297       }
298       layout = layout.parent;
299    }
300    return null;
301 }
302 
303 /**
304  * 
305  * @param {String} name
306  * @returns {helma.File}
307  */
308 Layout.prototype.getFile = function(name) {
309    name || (name = String.EMPTY);
310    return this.site.getStaticFile("layout/" + name);
311 }
312 
313 /**
314  * @returns {String[]}
315  */
316 Layout.prototype.getSkinPath = function() {
317    if (!this.site) {
318       return null;
319    }
320    var skinPath = [this.getFile().toString()];
321    return skinPath;
322 }
323 
324 /**
325  * 
326  */
327 Layout.prototype.reset = function() {
328    var skinFiles = app.getSkinfilesInPath([app.dir]);
329    var content, dir, file;
330    for (var name in skinFiles) {
331       if (content = skinFiles[name][name]) {
332          dir = this.getFile(name);
333          file = new helma.File(dir, name + ".skin");
334          dir.makeDirectory();
335          file.open();
336          file.write(content);
337          file.close();
338       }
339    }
340 
341    // FIXME: Reset the Site skin of root separately
342    content = skinFiles.Root.Site;
343    file = new helma.File(this.getFile("Root"), "Site.skin");
344    dir.makeDirectory();
345    file.open();
346    file.write(content);
347    file.close()
348 
349    this.touch();
350    return;
351 }
352 
353 /**
354  * 
355  * @param {String} skinPath
356  * @returns {helma.Zip}
357  */
358 Layout.prototype.getArchive = function(skinPath) {
359    var zip = new helma.Zip();
360    var skinFiles = app.getSkinfilesInPath(skinPath);
361    for (var name in skinFiles) {
362       if (skinFiles[name][name]) {
363          var file = new helma.File(this.getFile(name), name + ".skin");
364          if (file.exists()) {
365             zip.add(file, name);
366          }
367       }
368    }
369 
370    // FIXME: Add the Site skin of root separately
371    file = new helma.File(this.getFile("Root"), "Site.skin");
372    file.exists() && zip.add(file, "Root");
373 
374    var data = new HopObject;
375    data.images = new HopObject;
376    this.images.forEach(function() {
377       zip.add(this.getFile());
378       try {
379          zip.add(this.getThumbnailFile());
380       } catch (ex) {
381          /* Most likely the thumbnail file is identical to the image */ 
382       }
383       var image = new HopObject;
384       for each (var key in Image.KEYS) {
385          image[key] = this[key];
386          data.images.add(image);
387       }
388    });
389       
390    data.version = Root.VERSION;
391    data.origin = this.origin || this.site.href();
392    data.originator = this.originator || session.user.name;
393    data.originated = this.originated || new Date;
394    
395    // FIXME: XML encoder is losing all mixed-case properties :(
396    var xml = new java.lang.String(Xml.writeToString(data));
397    zip.addData(xml.getBytes("UTF-8"), "data.xml");
398    zip.close();
399    return zip;
400 }
401 
402 
403 /**
404  * @returns {String}
405  */
406 Layout.prototype.getConfirmText = function() {
407    return gettext("You are about to reset the layout of site {0}.", 
408          this.site.name);
409 }
410 /**
411  * 
412  * @param {String} name
413  * @returns {HopObject}
414  */
415 Layout.prototype.getMacroHandler = function(name) {
416    switch (name) {
417       case "skins":
418       return this[name];
419       
420       default:
421       return null;
422    }
423 }
424 
425 /**
426  * 
427  * @param {Object} param
428  * @param {String} name
429  * @param {String} mode
430  */
431 Layout.prototype.image_macro = function(param, name, mode) {
432    name || (name = param.name);
433    if (!name) {
434       return;
435    }
436 
437    var image = this.getImage(name, param.fallback);
438    if (!image) {
439       return;
440    }
441 
442    mode || (mode = param.as);
443    var action = param.linkto;
444    delete(param.name);
445    delete(param.as);
446    delete(param.linkto);
447 
448    switch (mode) {
449       case "url" :
450       return res.write(image.getUrl());
451       case "thumbnail" :
452       action || (action = image.getUrl());
453       return image.thumbnail_macro(param);
454    }
455    image.render_macro(param);
456    return;
457 }
458 
459 /**
460  * 
461  */
462 Layout.prototype.values_macro = function() {
463    var values = [];
464    for (var key in res.meta.values) {
465       values.push({key: key, value: res.meta.values[key]});
466    }
467    values.sort(new String.Sorter("key"));
468    for each (var pair in values) {
469       this.renderSkin("$Layout#value", {
470          key: pair.key.capitalize(), 
471          value: pair.value
472       });
473    }
474    return;
475 }
476