001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020package org.apache.isis.core.metamodel.specloader.specimpl.dflt; 021 022import java.lang.reflect.Array; 023import java.lang.reflect.Method; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Properties; 028import com.google.common.collect.Lists; 029import com.google.common.collect.Maps; 030import org.slf4j.Logger; 031import org.slf4j.LoggerFactory; 032import org.apache.isis.applib.filter.Filter; 033import org.apache.isis.applib.filter.Filters; 034import org.apache.isis.applib.profiles.Perspective; 035import org.apache.isis.core.commons.debug.DebugBuilder; 036import org.apache.isis.core.commons.debug.DebuggableWithTitle; 037import org.apache.isis.core.commons.exceptions.IsisException; 038import org.apache.isis.core.commons.lang.StringExtensions; 039import org.apache.isis.core.commons.util.ToString; 040import org.apache.isis.core.metamodel.adapter.ObjectAdapter; 041import org.apache.isis.core.metamodel.adapter.mgr.AdapterManager; 042import org.apache.isis.core.metamodel.facetapi.Facet; 043import org.apache.isis.core.metamodel.facetapi.FacetHolder; 044import org.apache.isis.core.metamodel.facets.FacetedMethod; 045import org.apache.isis.core.metamodel.facets.ImperativeFacet; 046import org.apache.isis.core.metamodel.facets.named.NamedFacet; 047import org.apache.isis.core.metamodel.facets.named.NamedFacetInferred; 048import org.apache.isis.core.metamodel.facets.object.callbacks.CallbackUtils; 049import org.apache.isis.core.metamodel.facets.object.callbacks.CreatedCallbackFacet; 050import org.apache.isis.core.metamodel.facets.object.icon.IconFacet; 051import org.apache.isis.core.metamodel.facets.object.plural.PluralFacet; 052import org.apache.isis.core.metamodel.facets.object.plural.PluralFacetInferred; 053import org.apache.isis.core.metamodel.facets.object.title.TitleFacet; 054import org.apache.isis.core.metamodel.facets.object.value.ValueFacet; 055import org.apache.isis.core.metamodel.runtimecontext.ServicesInjector; 056import org.apache.isis.core.metamodel.spec.*; 057import org.apache.isis.core.metamodel.spec.feature.*; 058import org.apache.isis.core.metamodel.specloader.classsubstitutor.ClassSubstitutor; 059import org.apache.isis.core.metamodel.specloader.specimpl.*; 060 061public class ObjectSpecificationDefault extends ObjectSpecificationAbstract implements DebuggableWithTitle, FacetHolder { 062 063 private final static Logger LOG = LoggerFactory.getLogger(ObjectSpecificationDefault.class); 064 065 private static String determineShortName(final Class<?> introspectedClass) { 066 final String name = introspectedClass.getName(); 067 return name.substring(name.lastIndexOf('.') + 1); 068 } 069 070 // ////////////////////////////////////////////////////////////// 071 // fields 072 // ////////////////////////////////////////////////////////////// 073 074 private boolean isService; 075 076 /** 077 * Lazily built by {@link #getMember(Method)}. 078 */ 079 private Map<Method, ObjectMember> membersByMethod = null; 080 081 private final IntrospectionContext introspectionContext; 082 private final CreateObjectContext createObjectContext; 083 084 private final FacetedMethodsBuilder facetedMethodsBuilder; 085 086 087 // ////////////////////////////////////////////////////////////////////// 088 // Constructor 089 // ////////////////////////////////////////////////////////////////////// 090 091 public ObjectSpecificationDefault( 092 final Class<?> correspondingClass, 093 final FacetedMethodsBuilderContext facetedMethodsBuilderContext, 094 final IntrospectionContext introspectionContext, 095 final SpecificationContext specContext, 096 final ObjectMemberContext objectMemberContext, 097 final CreateObjectContext createObjectContext) { 098 super(correspondingClass, determineShortName(correspondingClass), specContext, objectMemberContext); 099 100 this.facetedMethodsBuilder = new FacetedMethodsBuilder(this, facetedMethodsBuilderContext); 101 102 this.introspectionContext = introspectionContext; 103 this.createObjectContext = createObjectContext; 104 } 105 106 @Override 107 public void introspectTypeHierarchyAndMembers() { 108 109 metadataProperties = null; 110 if(isNotIntrospected()) { 111 metadataProperties = facetedMethodsBuilder.introspectClass(); 112 } 113 114 // name 115 if(isNotIntrospected()) { 116 addNamedFacetAndPluralFacetIfRequired(); 117 } 118 119 // go no further if a value 120 if(this.containsFacet(ValueFacet.class)) { 121 if (LOG.isDebugEnabled()) { 122 LOG.debug("skipping full introspection for value type " + getFullIdentifier()); 123 } 124 return; 125 } 126 127 // superclass 128 if(isNotIntrospected()) { 129 final Class<?> superclass = getCorrespondingClass().getSuperclass(); 130 updateSuperclass(superclass); 131 } 132 133 134 // walk superinterfaces 135 136 // 137 // REVIEW: the processing here isn't quite the same as with 138 // superclasses, in that with superclasses the superclass adds this type as its 139 // subclass, whereas here this type defines itself as the subtype. 140 // 141 // it'd be nice to push the responsibility for adding subclasses to 142 // the interface type... needs some tests around it, though, before 143 // making that refactoring. 144 // 145 final Class<?>[] interfaceTypes = getCorrespondingClass().getInterfaces(); 146 final List<ObjectSpecification> interfaceSpecList = Lists.newArrayList(); 147 for (final Class<?> interfaceType : interfaceTypes) { 148 final Class<?> substitutedInterfaceType = getClassSubstitutor().getClass(interfaceType); 149 if (substitutedInterfaceType != null) { 150 final ObjectSpecification interfaceSpec = getSpecificationLookup().loadSpecification(substitutedInterfaceType); 151 interfaceSpecList.add(interfaceSpec); 152 } 153 } 154 155 if(isNotIntrospected()) { 156 updateAsSubclassTo(interfaceSpecList); 157 } 158 if(isNotIntrospected()) { 159 updateInterfaces(interfaceSpecList); 160 } 161 162 // associations and actions 163 if(isNotIntrospected()) { 164 final List<ObjectAssociation> associations = createAssociations(metadataProperties); 165 sortAndUpdateAssociations(associations); 166 } 167 168 if(isNotIntrospected()) { 169 final List<ObjectAction> actions = createActions(metadataProperties); 170 sortCacheAndUpdateActions(actions); 171 } 172 173 if(isNotIntrospected()) { 174 facetedMethodsBuilder.introspectClassPostProcessing(metadataProperties); 175 } 176 177 if(isNotIntrospected()) { 178 updateFromFacetValues(); 179 } 180 } 181 182 private void addNamedFacetAndPluralFacetIfRequired() { 183 NamedFacet namedFacet = getFacet(NamedFacet.class); 184 if (namedFacet == null) { 185 namedFacet = new NamedFacetInferred(StringExtensions.asNaturalName2(getShortIdentifier()), this); 186 addFacet(namedFacet); 187 } 188 189 PluralFacet pluralFacet = getFacet(PluralFacet.class); 190 if (pluralFacet == null) { 191 pluralFacet = new PluralFacetInferred(StringExtensions.asPluralName(namedFacet.value()), this); 192 addFacet(pluralFacet); 193 } 194 } 195 196 // ////////////////////////////////////////////////////////////////////// 197 // create associations and actions 198 // ////////////////////////////////////////////////////////////////////// 199 200 private List<ObjectAssociation> createAssociations(Properties properties) { 201 final List<FacetedMethod> associationFacetedMethods = facetedMethodsBuilder.getAssociationFacetedMethods(properties); 202 final List<ObjectAssociation> associations = Lists.newArrayList(); 203 for (FacetedMethod facetedMethod : associationFacetedMethods) { 204 final ObjectAssociation association = createAssociation(facetedMethod); 205 if(association != null) { 206 associations.add(association); 207 } 208 } 209 return associations; 210 } 211 212 213 private ObjectAssociation createAssociation(final FacetedMethod facetMethod) { 214 if (facetMethod.getFeatureType().isCollection()) { 215 return new OneToManyAssociationImpl(facetMethod, objectMemberContext); 216 } else if (facetMethod.getFeatureType().isProperty()) { 217 return new OneToOneAssociationImpl(facetMethod, objectMemberContext); 218 } else { 219 return null; 220 } 221 } 222 223 private List<ObjectAction> createActions(Properties metadataProperties) { 224 final List<FacetedMethod> actionFacetedMethods = facetedMethodsBuilder.getActionFacetedMethods(metadataProperties); 225 final List<ObjectAction> actions = Lists.newArrayList(); 226 for (FacetedMethod facetedMethod : actionFacetedMethods) { 227 final ObjectAction action = createAction(facetedMethod); 228 if(action != null) { 229 actions.add(action); 230 } 231 } 232 return actions; 233 } 234 235 236 private ObjectAction createAction(final FacetedMethod facetedMethod) { 237 if (facetedMethod.getFeatureType().isAction()) { 238 return new ObjectActionImpl(facetedMethod, objectMemberContext); 239 } else { 240 return null; 241 } 242 } 243 244 245 // ////////////////////////////////////////////////////////////////////// 246 // Whether a service or not 247 // ////////////////////////////////////////////////////////////////////// 248 249 @Override 250 public boolean isService() { 251 return isService; 252 } 253 254 /** 255 * TODO: should ensure that service has at least one user action; fix when 256 * specification knows of its hidden methods. 257 * 258 * <pre> 259 * if (objectActions != null && objectActions.length == 0) { 260 * throw new ObjectSpecificationException("Service object " + getFullName() + " should have at least one user action"); 261 * } 262 * </pre> 263 */ 264 @Override 265 public void markAsService() { 266 ensureServiceHasNoAssociations(); 267 isService = true; 268 } 269 270 private void ensureServiceHasNoAssociations() { 271 final List<ObjectAssociation> associations = getAssociations(Contributed.EXCLUDED); 272 final StringBuilder buf = new StringBuilder(); 273 for (final ObjectAssociation association : associations) { 274 final String name = association.getId(); 275 // services are allowed to have one association, called 'id' 276 if (!isValidAssociationForService(name)) { 277 appendAssociationName(buf, name); 278 } 279 } 280 if (buf.length() > 0) { 281 throw new ObjectSpecificationException("Service object " + getFullIdentifier() + " should have no fields, but has: " + buf); 282 } 283 } 284 285 /** 286 * Services are allowed to have one association, called 'id'. 287 * 288 * <p> 289 * This is used for {@link Perspective}s (user profiles). 290 */ 291 private boolean isValidAssociationForService(final String associationId) { 292 return "id".indexOf(associationId) != -1; 293 } 294 295 private void appendAssociationName(final StringBuilder fieldNames, final String name) { 296 fieldNames.append(fieldNames.length() > 0 ? ", " : ""); 297 fieldNames.append(name); 298 } 299 300 // ////////////////////////////////////////////////////////////////////// 301 // getObjectAction 302 // ////////////////////////////////////////////////////////////////////// 303 304 @Override 305 public ObjectAction getObjectAction(final ActionType type, final String id, final List<ObjectSpecification> parameters) { 306 final List<ObjectAction> actions = 307 getObjectActions(type, Contributed.INCLUDED, Filters.<ObjectAction>any()); 308 return firstAction(actions, id, parameters); 309 } 310 311 @Override 312 public ObjectAction getObjectAction(final ActionType type, final String id) { 313 final List<ObjectAction> actions = 314 getObjectActions(type, Contributed.INCLUDED, Filters.<ObjectAction>any()); 315 return firstAction(actions, id); 316 } 317 318 @Override 319 public ObjectAction getObjectAction(final String id) { 320 final List<ObjectAction> actions = 321 getObjectActions(ActionType.ALL, Contributed.INCLUDED, Filters.<ObjectAction>any()); 322 return firstAction(actions, id); 323 } 324 325 private static ObjectAction firstAction( 326 final List<ObjectAction> candidateActions, 327 final String actionName, 328 final List<ObjectSpecification> parameters) { 329 outer: for (int i = 0; i < candidateActions.size(); i++) { 330 final ObjectAction action = candidateActions.get(i); 331 if (actionName != null && !actionName.equals(action.getId())) { 332 continue outer; 333 } 334 if (action.getParameters().size() != parameters.size()) { 335 continue outer; 336 } 337 for (int j = 0; j < parameters.size(); j++) { 338 if (!parameters.get(j).isOfType(action.getParameters().get(j).getSpecification())) { 339 continue outer; 340 } 341 } 342 return action; 343 } 344 return null; 345 } 346 347 private static ObjectAction firstAction( 348 final List<ObjectAction> candidateActions, 349 final String id) { 350 if (id == null) { 351 return null; 352 } 353 for (int i = 0; i < candidateActions.size(); i++) { 354 final ObjectAction action = candidateActions.get(i); 355 if (id.equals(action.getIdentifier().toNameParmsIdentityString())) { 356 return action; 357 } 358 if (id.equals(action.getIdentifier().toNameIdentityString())) { 359 return action; 360 } 361 continue; 362 } 363 return null; 364 } 365 366 // ////////////////////////////////////////////////////////////////////// 367 // createObject 368 // ////////////////////////////////////////////////////////////////////// 369 370 @Override 371 public Object createObject() { 372 if (getCorrespondingClass().isArray()) { 373 return Array.newInstance(getCorrespondingClass().getComponentType(), 0); 374 } 375 376 try { 377 return getObjectInstantiator().instantiate(getCorrespondingClass()); 378 } catch (final ObjectInstantiationException e) { 379 throw new IsisException("Failed to create instance of type " + getFullIdentifier(), e); 380 } 381 } 382 383 /** 384 * REVIEW: does this behaviour live best here? Not that sure that it does... 385 */ 386 @Override 387 public ObjectAdapter initialize(final ObjectAdapter adapter) { 388 389 // initialize new object 390 final List<ObjectAssociation> fields = adapter.getSpecification().getAssociations(Contributed.EXCLUDED); 391 for (ObjectAssociation field : fields) { 392 field.toDefault(adapter); 393 } 394 getDependencyInjector().injectServicesInto(adapter.getObject()); 395 396 CallbackUtils.callCallback(adapter, CreatedCallbackFacet.class); 397 398 return adapter; 399 } 400 401 402 // ////////////////////////////////////////////////////////////////////// 403 // getMember, catalog... (not API) 404 // ////////////////////////////////////////////////////////////////////// 405 406 public ObjectMember getMember(final Method method) { 407 if (membersByMethod == null) { 408 this.membersByMethod = catalogueMembers(); 409 } 410 return membersByMethod.get(method); 411 } 412 413 private HashMap<Method, ObjectMember> catalogueMembers() { 414 final HashMap<Method, ObjectMember> membersByMethod = Maps.newHashMap(); 415 cataloguePropertiesAndCollections(membersByMethod); 416 catalogueActions(membersByMethod); 417 return membersByMethod; 418 } 419 420 private void cataloguePropertiesAndCollections(final Map<Method, ObjectMember> membersByMethod) { 421 final Filter<ObjectAssociation> noop = Filters.anyOfType(ObjectAssociation.class); 422 final List<ObjectAssociation> fields = getAssociations(Contributed.EXCLUDED, noop); 423 for (int i = 0; i < fields.size(); i++) { 424 final ObjectAssociation field = fields.get(i); 425 final List<Facet> facets = field.getFacets(ImperativeFacet.FILTER); 426 for (final Facet facet : facets) { 427 final ImperativeFacet imperativeFacet = ImperativeFacet.Util.getImperativeFacet(facet); 428 for (final Method imperativeFacetMethod : imperativeFacet.getMethods()) { 429 membersByMethod.put(imperativeFacetMethod, field); 430 } 431 } 432 } 433 } 434 435 private void catalogueActions(final Map<Method, ObjectMember> membersByMethod) { 436 final List<ObjectAction> userActions = getObjectActions(Contributed.INCLUDED); 437 for (int i = 0; i < userActions.size(); i++) { 438 final ObjectAction userAction = userActions.get(i); 439 final List<Facet> facets = userAction.getFacets(ImperativeFacet.FILTER); 440 for (final Facet facet : facets) { 441 final ImperativeFacet imperativeFacet = ImperativeFacet.Util.getImperativeFacet(facet); 442 for (final Method imperativeFacetMethod : imperativeFacet.getMethods()) { 443 membersByMethod.put(imperativeFacetMethod, userAction); 444 } 445 } 446 } 447 } 448 449 // ////////////////////////////////////////////////////////////////////// 450 // Debug, toString 451 // ////////////////////////////////////////////////////////////////////// 452 453 @Override 454 public void debugData(final DebugBuilder debug) { 455 debug.blankLine(); 456 debug.appendln("Title", getFacet(TitleFacet.class)); 457 final IconFacet iconFacet = getFacet(IconFacet.class); 458 if (iconFacet != null) { 459 debug.appendln("Icon", iconFacet); 460 } 461 debug.unindent(); 462 } 463 464 @Override 465 public String debugTitle() { 466 return "NO Member Specification"; 467 } 468 469 @Override 470 public String toString() { 471 final ToString str = new ToString(this); 472 str.append("class", getFullIdentifier()); 473 str.append("type", (isParentedOrFreeCollection() ? "Collection" : "Object")); 474 str.append("persistable", persistability()); 475 str.append("superclass", superclass() == null ? "Object" : superclass().getFullIdentifier()); 476 return str.toString(); 477 } 478 479 // ////////////////////////////////////////////////////////////////// 480 // Dependencies (from constructor) 481 // ////////////////////////////////////////////////////////////////// 482 483 protected AdapterManager getAdapterMap() { 484 return createObjectContext.getAdapterManager(); 485 } 486 487 protected ServicesInjector getDependencyInjector() { 488 return createObjectContext.getDependencyInjector(); 489 } 490 491 private ClassSubstitutor getClassSubstitutor() { 492 return introspectionContext.getClassSubstitutor(); 493 } 494 495 496}