view-impl.ts 9.06 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*******************************************************************************
 * Copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 * contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 *
 * This file is part of CubicWeb.
 *
 * CubicWeb is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * CubicWeb is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

Laurent Wouters's avatar
Laurent Wouters committed
21
import * as $rdf from "rdflib";
22
23
24
import {
  ViewRenderer,
  ViewRendering,
Laurent Wouters's avatar
Laurent Wouters committed
25
26
  RenderingContext,
  Language
27
} from "./application";
28
29
30
import {
  ViewDescriptor,
  ViewRegistry,
31
  ViewResourceContentResolver
32
} from "./view-def";
33

34
35
36
37
38
/**
 * Priority for a view that is not appropriate (must not be selected)
 */
export const VIEW_PRIORITY_INAPPROPRIATE = -1;

39
40
41
42
43
44
45
46
47
48
/**
 * The implementation of a view
 */
export interface ViewImplementation {
  /**
   * The descriptor for ths implementation
   */
  descriptor: ViewDescriptor;

  /**
49
50
51
   * Gets the view's priority
   * - 0 to Number.MAX_SAFE_INTEGER for valid view with decreasing priority
   * - -1 for inappropriate views (must not be selected)
52
53
54
   * @param store The RDF store holding the data to be rendered
   * @param target The target resource to be rendered
   */
Laurent Wouters's avatar
Laurent Wouters committed
55
  priorityFor(store: $rdf.Formula, target: string): number;
56
57
58
  /**
   * Renders a resource belonging to an RDF store
   * @param renderer The parent renderer
59
   * @param context The current rendering context
60
61
62
63
   * @param target The target resource for this view
   */
  render(
    renderer: ViewRenderer,
64
    context: RenderingContext,
65
    target: string
66
  ): HTMLElement;
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
}

/**
 * A repository of view implementations
 */
export interface ViewImplementations {
  [id: string]: ViewImplementation;
}

/**
 * The global reposistory of embedded view implementations
 */
export const IMPL_EMBEDDED: ViewImplementations = {};

/**
 * Registers an embedded view implementation
 * @param implementation A view implementation
 */
export function registerImplementation(implementation: ViewImplementation) {
  IMPL_EMBEDDED[implementation.descriptor.identifier] = implementation;
}

/**
 * Fetches the implementation corresponding to a descriptor
 * @param descriptor The descriptor
92
 * @param resolver The content resolver to use
93
94
 */
function fetchImplementation(
95
96
  descriptor: ViewDescriptor,
  resolver: ViewResourceContentResolver
97
): Promise<ViewImplementation> {
98
99
100
101
102
  return new Promise(
    (
      resolve: (impl: ViewImplementation) => any,
      reject: (reason: string) => void
    ) => {
103
      resolver(descriptor.resourceMain)
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
        .then((content: string) => {
          let EMBEDDED = IMPL_EMBEDDED;
          let result = eval(content);
          if (result == null || result == undefined) {
            reject(
              "Failed to load definition of " +
                descriptor.name +
                ": Cannot find entrypoint"
            );
            return;
          }
          if (
            descriptor.entrypoint != null &&
            descriptor.entrypoint.length > 0
          ) {
            result = result[descriptor.entrypoint];
          }
          if (result == null || result == undefined) {
            reject(
              "Failed to load definition of " +
                descriptor.name +
                ": Cannot find entrypoint"
            );
          } else {
            resolve(result);
          }
        })
        .catch((reason: string) => {
          reject(
            "Failed to fetch definition of " +
              descriptor.name +
              ": Cannot find entrypoint"
          );
        });
138
139
    }
  );
140
141
142
143
144
}

/**
 * Loads all implementations for the registered views
 * @param registry The registry to load
145
 * @param resolver The content resolver to use
146
147
 */
function loadImplementations(
148
149
  registry: ViewRegistry,
  resolver: ViewResourceContentResolver
150
151
152
153
154
155
156
157
158
): Promise<ViewImplementations> {
  return new Promise<ViewImplementations>(
    (
      resolve: (impls: ViewImplementations) => void,
      reject: (reason: any) => void
    ) => {
      var finished = 0;
      var target = Object.keys(registry.descriptors).length;
      var implementations: ViewImplementations = {};
159
      function onPartFinished() {
160
161
        finished += 1;
        if (finished >= target) resolve(implementations);
162
      }
163
164
      Object.keys(registry.descriptors).map((id: string) => {
        let descriptor = registry.descriptors[id];
165
        fetchImplementation(descriptor, resolver)
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
          .then((implementation: ViewImplementation) => {
            implementations[id] = implementation;
            onPartFinished();
          })
          .catch((reason: string) => {
            console.log(
              "Failed to fetch implementation for view " +
                descriptor.name +
                ": " +
                reason
            );
            onPartFinished();
          });
      });
    }
  );
}

/**
 * A prioritized view implementation
 */
interface PrioritizedViewImplementation {
  /**
   * The related implementation
   */
  implementation: ViewImplementation;
  /**
   * The associated priority
   */
  priority: number;
}

/**
 * Resoles a view for an RDF datastore and a resource in it
 * @param implementations The view implementations to use
 * @param store The RDF store holding the data to be rendered
 * @param target The target RDF resource to render
 */
function resolveViewFor(
  implementations: ViewImplementations,
Laurent Wouters's avatar
Laurent Wouters committed
206
  store: $rdf.Formula,
207
208
209
210
211
212
  target: string
): ViewImplementation {
  let sorted: PrioritizedViewImplementation[] = Object.keys(implementations)
    .map((id: string) => {
      let impl = implementations[id];
      return {
213
        implementation: impl,
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
        priority: impl.priorityFor(store, target)
      };
    })
    .filter((a: PrioritizedViewImplementation) => a.priority >= 0)
    .sort(
      (x: PrioritizedViewImplementation, y: PrioritizedViewImplementation) =>
        x.priority - y.priority
    );
  if (sorted.length == 0) return null;
  return sorted[0].implementation;
}

/**
 * A renderer for an application
 */
class ViewRendererImpl implements ViewRenderer {
  /**
   * Initializes a renderer
   * @param registry The registry to use
   * @param implementations The view implementations
   */
235
  constructor(registry: ViewRegistry, implementations: ViewImplementations) {
236
237
238
239
240
241
242
243
244
245
246
247
    this.registry = registry;
    this.implementations = implementations;
  }

  /**
   * The registry to use
   */
  registry: ViewRegistry;
  /**
   * The view implementations
   */
  implementations: ViewImplementations;
248

Laurent Wouters's avatar
Laurent Wouters committed
249
250
251
252
253
  /**
   * Selects the language to use for a target resource
   * @param context The current rendering context
   * @param target The target resource for this view
   */
254
  getLanguagesFor(context: RenderingContext, target: string): Language[] {
Laurent Wouters's avatar
Laurent Wouters committed
255
    let options = context.options[target];
256
257
258
259
260
261
262
263
264
265
266
    let result = [];
    if (
      options != undefined &&
      options != null &&
      options.language != undefined &&
      options.language != null
    )
      result.push(options.language);
    if (context.browserLanguage != null && context.browserLanguage != undefined)
      result.push(context.browserLanguage);
    return result;
Laurent Wouters's avatar
Laurent Wouters committed
267
268
  }

269
270
  /**
   * Renders a resource belonging to an RDF store
271
   * @param context The context for the rendering
272
273
   * @param target The target resource for this view
   */
274
  render(context: RenderingContext, target: string): ViewRendering {
275
    var view = null;
276
277
278
279
280
281
282
    let options = context.options[target];
    if (
      options != null &&
      options != undefined &&
      options.view != undefined &&
      options.view != undefined
    ) {
283
      // try to use the forced view
284
      view = view = this.implementations[options.view];
285
286
287
    }
    if (view == null || view == undefined) {
      // try to resolve a view
288
      view = resolveViewFor(this.implementations, context.store, target);
289
290
291
292
293
    }
    if (view == null || view == undefined) {
      // cannot resolve a view ...
      return null;
    }
294
    return {
295
      dom: view.render(this, context, target),
296
297
      viewId: view.descriptor.identifier
    };
298
299
300
301
302
303
  }
}

/**
 * Creates a new view renderer
 * @param registry The view registry to use
304
 * @param resolver The content resolver to use
305
306
 */
export function newRenderer(
307
  registry: ViewRegistry,
308
  resolver: ViewResourceContentResolver
309
310
311
312
313
314
): Promise<ViewRenderer> {
  return new Promise<ViewRenderer>(
    (
      resolve: (renderer: ViewRenderer) => void,
      reject: (reason: any) => void
    ) => {
315
      loadImplementations(registry, resolver).then(
316
        (impls: ViewImplementations) => {
317
          let renderer = new ViewRendererImpl(registry, impls);
318
319
320
          resolve(renderer);
        }
      );
321
322
323
    }
  );
}