001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.dataAccess;
019
020import com.fasterxml.jackson.annotation.JsonIgnore;
021import io.stallion.Context;
022import io.stallion.dataAccess.db.Converter;
023import io.stallion.dataAccess.db.converters.JsonListConverter;
024import io.stallion.settings.SecondaryDomain;
025import io.stallion.settings.Settings;
026import io.stallion.utils.DateUtils;
027import io.stallion.utils.GeneralUtils;
028
029import javax.persistence.Column;
030import java.time.ZonedDateTime;
031import java.time.format.DateTimeFormatter;
032import java.util.List;
033
034import static io.stallion.Context.settings;
035import static io.stallion.utils.Literals.empty;
036import static io.stallion.utils.Literals.list;
037
038/**
039 * A base class for model objects that can be rendered as web pages.
040 */
041public class StandardDisplayableModel extends ModelBase implements Displayable {
042    // Unfortunately these fields all have to be null, so when the object is passed in from
043    // a JSON web request, we can identify which fields are set and which are not
044    private String title;
045    private String slug;
046    private String content;
047    private String originalContent;
048    private String template;
049    private ZonedDateTime publishDate;
050    private Boolean draft = false;
051    private String author = "";
052    private String metaDescription = "";
053    private String overrideDomain = null;
054    private String relCanonical = "";
055    private String metaKeywords = "";
056    private String titleTag = "";
057    private String image = "";
058    private String ogType = "article";
059    private String previewKey = "";
060    private List<String> oldUrls = list();
061    private String contentType = "";
062
063    /**
064     * Used in the page &lt;title&gt; tag.
065     *
066     * @return
067     */
068    @Column
069    public String getTitle() {
070        return title;
071    }
072
073    public StandardDisplayableModel setTitle(String title) {
074        this.title = title;
075        return this;
076    }
077
078    /**
079     * The user settable path component of the URL at which this page can be accessed, before
080     * applying the override/secondary domain (if applicable)
081     *
082     * @return
083     */
084    @Override
085    @Column
086    @UniqueKey
087    public String getSlug() {
088        return this.slug;
089    }
090
091    /**
092     * The path component of the URL at which this page can be accessed. This is the slug by default,
093     * but if on an override domain, the slug minus the root
094     */
095    @Override
096    public String getRealHttpPath() {
097        if (empty(getOverrideDomain())) {
098            return getSlug();
099        } else {
100            SecondaryDomain sd = settings().getSecondaryDomainByDomain(getOverrideDomain());
101            if (sd.isStripRootFromPageSlug()) {
102                return getSlug().substring(sd.getRewriteRoot().length());
103            } else {
104                return getSlug();
105            }
106        }
107    }
108
109    /**
110     * Empty by default, but if secondary domains are found, and the page slug matches that of a secondary domain,
111     * then this method will return the secondary domain.
112     *
113     * @return
114     */
115    @Override
116    @JsonIgnore
117    public String getOverrideDomain() {
118        if (overrideDomain != null) {
119            return overrideDomain;
120        }
121        if (Settings.instance() == null) {
122            overrideDomain = "";
123            return overrideDomain;
124        }
125        if (Settings.instance().getSecondaryDomains().size() == 0) {
126            overrideDomain = "";
127            return overrideDomain;
128        }
129        if (getSlug() == null) {
130            overrideDomain = "";
131            return overrideDomain;
132        }
133        for (SecondaryDomain d : Settings.instance().getSecondaryDomains()) {
134            if (getSlug().startsWith(d.getRewriteRoot())) {
135                overrideDomain = d.getDomain();
136                return overrideDomain;
137            }
138        }
139        overrideDomain = "";
140        return overrideDomain;
141    }
142
143    /**
144     * Creates a CSS id for the page &lt;body&gt; tag based on the slug. This is used
145     * to make it easy to do per-page stylings in a stylesheet.
146     *
147     * @return
148     */
149    public String getSlugForCssId() {
150        String s = slug;
151        if (slug == null) {
152            return "";
153        }
154        if (s.startsWith("/")) {
155            s = s.substring(1);
156        }
157        if (empty(s)) {
158            s = "site-root";
159        }
160        s = GeneralUtils.slugify(s);
161        return s;
162    }
163
164    /**
165     * The actual page content that gets displayed in the body of the web page. This should
166     * already be HTML (for HTML pages).
167     *
168     * @return
169     */
170    @Override
171    @Column(columnDefinition = "longtext")
172    public String getContent() {
173        return this.content;
174    }
175
176    public <D extends StandardDisplayableModel> D setSlug(String slug) {
177        this.slug = slug;
178        return (D) this;
179    }
180
181    public <D extends StandardDisplayableModel> D setContent(String content) {
182        this.content = content;
183        return (D) this;
184    }
185
186    /**
187     * The path of the template used to render the page. Will default to page.jinja if it is not set.
188     *
189     * @return
190     */
191    @Override
192    @Column
193    public String getTemplate() {
194        return template;
195    }
196
197    public <D extends StandardDisplayableModel> D setTemplate(String template) {
198        this.template = template;
199        return (D) this;
200    }
201
202
203    /**
204     * Get the date at which the page should be live, if this is in the future, the page will
205     * not be visible.
206     *
207     * @return
208     */
209    @Column
210    public ZonedDateTime getPublishDate() {
211        return publishDate;
212    }
213
214    public <D extends StandardDisplayableModel> D setPublishDate(ZonedDateTime publishDate) {
215        this.publishDate = publishDate;
216        return (D) this;
217    }
218
219    /**
220     * If true, the page will not be visible.
221     *
222     * @return
223     */
224    @Column
225    public Boolean getDraft() {
226        return draft;
227    }
228
229    public <D extends StandardDisplayableModel> D setDraft(Boolean draft) {
230        this.draft = draft;
231        return (D) this;
232    }
233
234    /**
235     * True if draft is false, and publishDate is before the current time.
236     *
237     * @return
238     */
239    public Boolean getPublished() {
240        if (draft == false && (getPublishDate() == null || getPublishDate().isBefore(DateUtils.utcNow()))) {
241            return true;
242        } else {
243            return false;
244        }
245    }
246
247    public ZonedDateTime getDate() {
248        return this.getPublishDate();
249    }
250
251    /**
252     * Get the publish date and format it with the passed in DateTimeFormatter
253     *
254     * @param format
255     * @return
256     */
257    public String formattedDate(String format) {
258        DateTimeFormatter formatter =
259                DateTimeFormatter.ofPattern(format);
260        return getPublishDate().format(formatter);
261    }
262
263    /**
264     * Get the full URL to this page
265     *
266     * @return
267     */
268    public String getPermalink() {
269        if (empty(getOverrideDomain())) {
270            return Context.getSettings().getSiteUrl() + getRealHttpPath();
271        } else {
272            String scheme = settings().getSchemeForSecondaryDomain(getOverrideDomain());
273
274            return scheme + "://" + getOverrideDomain() + getRealHttpPath();
275        }
276
277    }
278
279    /**
280     * Get the author name of this page.
281     *
282     * @return
283     */
284    @Column
285    public String getAuthor() {
286        return author;
287    }
288
289    public <D extends StandardDisplayableModel> D setAuthor(String author) {
290        this.author = author;
291        return (D) this;
292    }
293
294    /**
295     * For the description meta tag, often used by Google and others to give a one sentence
296     * explanation of the page in search results.
297     *
298     * @return
299     */
300    @Column
301    public String getMetaDescription() {
302        return metaDescription;
303    }
304
305    public <D extends StandardDisplayableModel> D setMetaDescription(String metaDescription) {
306        this.metaDescription = metaDescription;
307        return (D) this;
308    }
309
310    /**
311     * Used for the canonical tag in the HTML head section, set this manually if this page
312     * is accessible at multiple URL's and you do not want to get duplicate content penalties
313     * in Google.
314     *
315     * @return
316     */
317    @Override
318    @Column
319    public String getRelCanonical() {
320        return relCanonical;
321    }
322
323    public <D extends StandardDisplayableModel> D setRelCanonical(String relCanonical) {
324        this.relCanonical = relCanonical;
325        return (D) this;
326    }
327
328    /**
329     * Get a list of comma separated keywords for the keywords tag.
330     *
331     * @return
332     */
333    @Override
334    @Column
335    public String getMetaKeywords() {
336        return metaKeywords;
337    }
338
339    public <D extends StandardDisplayableModel> D setMetaKeywords(String metaKeywords) {
340        this.metaKeywords = metaKeywords;
341        return (D) this;
342    }
343
344
345    @Override
346    @Column
347    public String getTitleTag() {
348        return titleTag;
349    }
350
351    public <D extends StandardDisplayableModel> D setTitleTag(String titleTag) {
352        this.titleTag = titleTag;
353        return (D) this;
354    }
355
356    /**
357     * Get used for the og:image meta tag.
358     *
359     * @return
360     */
361    @Override
362    @Column
363    public String getImage() {
364        return image;
365    }
366
367    public <D extends StandardDisplayableModel> D setImage(String image) {
368        this.image = image;
369        return (D) this;
370    }
371
372    /**
373     * OpenGraph page type, for the HTML meta section
374     *
375     * @return
376     */
377    @Override
378    @Column(length = 30)
379    public String getOgType() {
380        return ogType;
381    }
382
383    public <D extends StandardDisplayableModel> D setOgType(String ogType) {
384        this.ogType = ogType;
385        return (D) this;
386    }
387
388    /**
389     * Set this to some custom value, and then you can view an unpublished page via
390     * /slug?stPreview=(your preview key)
391     *
392     * @return
393     */
394    @Override
395    @Column(length = 40)
396    public String getPreviewKey() {
397        return previewKey;
398    }
399
400    public <D extends StandardDisplayableModel> D setPreviewKey(String previewKey) {
401        this.previewKey = previewKey;
402        return (D) this;
403    }
404
405    /**
406     * If you change the URL of a page, add the old url here, Stallion will then 301 redirect
407     * the old url to the new url.
408     *
409     * @return
410     */
411    @Override
412    @Column(columnDefinition = "longtext")
413    @Converter(cls = JsonListConverter.class)
414    public List<String> getOldUrls() {
415        return oldUrls;
416    }
417
418    @Override
419    public <D extends Displayable> D setOldUrls(List<String> oldUrls) {
420        this.oldUrls = oldUrls;
421        return (D) this;
422    }
423
424    @Override
425    @Column(columnDefinition = "longtext")
426    public String getOriginalContent() {
427        return originalContent;
428    }
429
430    public <D extends StandardDisplayableModel> D setOriginalContent(String originalContent) {
431        this.originalContent = originalContent;
432        return (D) this;
433    }
434
435    public String getSummary() {
436        if (empty(getContent())) {
437            return "";
438        }
439        int i = getContent().indexOf("<!--more-->");
440        if (i > -1) {
441            return getContent().substring(0, i);
442        }
443        i = getContent().indexOf("</p>");
444        if (i > -1) {
445            return getContent().substring(0, i + 4);
446        }
447        i = getContent().indexOf("</div>");
448        if (i > -1) {
449            return getContent().substring(0, i + 6);
450        }
451        return getContent();
452    }
453
454    public String getTruncatedSummary(int max) {
455        String summary = getSummary();
456        if (summary.length() < max) {
457            return summary;
458        } else {
459            max = summary.lastIndexOf(" ", max);
460            return summary.substring(0, max) + "&hellip;";
461        }
462    }
463
464
465    public String formatPublishDate() {
466        return formatPublishDate("MMM d, YYYY");
467    }
468
469    public String formatPublishDate(String pattern) {
470        ZonedDateTime dt = getPublishDate();
471        if (dt == null) {
472            dt = DateUtils.utcNow();
473        }
474        // TODO: Localize
475        return this.getPublishDate().format(DateTimeFormatter.ofPattern(pattern));
476    }
477
478    @Override
479    public String getContentType() {
480        return contentType;
481    }
482
483    public StandardDisplayableModel setContentType(String contentType) {
484        this.contentType = contentType;
485        return this;
486    }
487}